ironcontext_core/
report.rs1use serde::{Deserialize, Serialize};
12
13use crate::manifest::Manifest;
14use crate::optimizer::{DescriptionOptimizer, HeuristicOptimizer, OptimizationOutcome};
15use crate::ris::{score_manifest, RisScore};
16use crate::rules::{run_all, Finding, Severity};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Report {
20 pub schema_version: u32,
21 pub server_name: String,
22 pub server_version: String,
23 pub findings: Vec<Finding>,
24 pub tools: Vec<ToolReport>,
25 pub summary: Summary,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ToolReport {
30 pub name: String,
31 pub ris: RisScore,
32 #[serde(skip_serializing_if = "Option::is_none", default)]
34 pub optimization: Option<OptimizationOutcome>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Summary {
39 pub total_tools: usize,
40 pub total_findings: usize,
41 pub findings_by_severity: std::collections::BTreeMap<String, usize>,
42 pub mean_ris: f32,
43 #[serde(skip_serializing_if = "Option::is_none", default)]
45 pub mean_token_reduction_pct: Option<f32>,
46}
47
48impl Report {
49 pub fn build_security(manifest: &Manifest) -> Self {
52 Self::assemble(manifest, None::<&HeuristicOptimizer>)
53 }
54
55 pub fn build_full(manifest: &Manifest) -> Self {
58 Self::assemble(manifest, Some(&HeuristicOptimizer::default()))
59 }
60
61 pub fn build_full_with<O: DescriptionOptimizer>(manifest: &Manifest, opt: &O) -> Self {
64 Self::assemble(manifest, Some(opt))
65 }
66
67 pub fn build(manifest: &Manifest) -> Self {
69 Self::build_security(manifest)
70 }
71
72 fn assemble<O: DescriptionOptimizer>(manifest: &Manifest, opt: Option<&O>) -> Self {
73 let findings = run_all(manifest);
74 let ris_scores = score_manifest(manifest);
75
76 let tools: Vec<ToolReport> = manifest
77 .tools
78 .iter()
79 .zip(ris_scores.into_iter())
80 .map(|(t, r)| ToolReport {
81 name: t.name.clone(),
82 ris: r,
83 optimization: opt.map(|o| o.rewrite(t)),
84 })
85 .collect();
86
87 let total_tools = manifest.tools.len();
88 let total_findings = findings.len();
89
90 let mut by_sev: std::collections::BTreeMap<String, usize> = Default::default();
91 for f in &findings {
92 *by_sev.entry(severity_key(f.severity).to_string()).or_default() += 1;
93 }
94
95 let mean_ris = if tools.is_empty() {
96 0.0
97 } else {
98 tools.iter().map(|t| t.ris.score as f32).sum::<f32>() / tools.len() as f32
99 };
100 let mean_reduction = if opt.is_some() && !tools.is_empty() {
101 Some(
102 tools
103 .iter()
104 .filter_map(|t| t.optimization.as_ref().map(|o| o.reduction_pct))
105 .sum::<f32>()
106 / tools.len() as f32,
107 )
108 } else {
109 None
110 };
111
112 Report {
113 schema_version: 1,
114 server_name: manifest.server.name.clone(),
115 server_version: manifest.server.version.clone(),
116 findings,
117 tools,
118 summary: Summary {
119 total_tools,
120 total_findings,
121 findings_by_severity: by_sev,
122 mean_ris,
123 mean_token_reduction_pct: mean_reduction,
124 },
125 }
126 }
127
128 pub fn exit_code(&self) -> i32 {
130 let bad = self
131 .findings
132 .iter()
133 .any(|f| matches!(f.severity, Severity::High | Severity::Critical));
134 if bad {
135 1
136 } else {
137 0
138 }
139 }
140}
141
142fn severity_key(s: Severity) -> &'static str {
143 match s {
144 Severity::Info => "info",
145 Severity::Low => "low",
146 Severity::Medium => "medium",
147 Severity::High => "high",
148 Severity::Critical => "critical",
149 }
150}