1use crate::core::engine::EngineResult;
2use crate::rules::core::{ArtifactCacheStats, RuleCategory, RuleStatus, Severity, RULESET_VERSION};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ReportData {
9 pub ruleset_version: String,
10 pub generated_at_unix: u64,
11 pub total_duration_ms: u128,
12 pub cache_stats: ArtifactCacheStats,
13 pub slow_rules: Vec<SlowRule>,
14 pub results: Vec<ReportItem>,
15 #[serde(default)]
16 pub scanned_targets: Vec<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ReportItem {
21 pub rule_id: String,
22 pub rule_name: String,
23 pub category: RuleCategory,
24 pub severity: Severity,
25 pub target: String,
26 pub status: RuleStatus,
27 pub message: Option<String>,
28 pub evidence: Option<String>,
29 pub recommendation: String,
30 pub duration_ms: u128,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct SlowRule {
35 pub rule_id: String,
36 pub rule_name: String,
37 pub duration_ms: u128,
38}
39
40#[derive(Debug, Clone)]
41pub struct BaselineSummary {
42 pub suppressed: usize,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AgentPack {
47 pub generated_at_unix: u64,
48 pub total_findings: usize,
49 pub findings: Vec<AgentFinding>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AgentFinding {
54 pub rule_id: String,
55 pub rule_name: String,
56 pub severity: Severity,
57 pub category: RuleCategory,
58 pub priority: String,
59 pub message: String,
60 pub evidence: Option<String>,
61 pub recommendation: String,
62 pub suggested_fix_scope: String,
63 pub target_files: Vec<String>,
64 pub patch_hint: String,
65 pub why_it_fails_review: String,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum AgentPackFormat {
70 Json,
71 Markdown,
72 Bundle,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum FailOn {
77 Off,
78 Error,
79 Warning,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum TimingMode {
84 Off,
85 Summary,
86 Full,
87}
88
89pub fn build_report(
90 results: Vec<EngineResult>,
91 total_duration_ms: u128,
92 cache_stats: ArtifactCacheStats,
93) -> ReportData {
94 let generated_at_unix = SystemTime::now()
95 .duration_since(UNIX_EPOCH)
96 .unwrap_or_default()
97 .as_secs();
98
99 let mut items = Vec::new();
100
101 for res in results {
102 let (status, message, evidence) = match res.report {
103 Ok(report) => (report.status, report.message, report.evidence),
104 Err(err) => (
105 RuleStatus::Error,
106 Some(err.to_string()),
107 Some("Rule evaluation error".to_string()),
108 ),
109 };
110
111 items.push(ReportItem {
112 rule_id: res.rule_id.to_string(),
113 rule_name: res.rule_name.to_string(),
114 category: res.category,
115 severity: res.severity,
116 target: res.target.clone(),
117 status,
118 message,
119 evidence,
120 recommendation: res.recommendation.to_string(),
121 duration_ms: res.duration_ms,
122 });
123 }
124
125 let mut scanned_targets: Vec<String> = items.iter().map(|i| i.target.clone()).collect();
126 scanned_targets.sort();
127 scanned_targets.dedup();
128
129 let report = ReportData {
130 ruleset_version: RULESET_VERSION.to_string(),
131 generated_at_unix,
132 total_duration_ms,
133 cache_stats,
134 slow_rules: Vec::new(),
135 results: items,
136 scanned_targets,
137 };
138
139 ReportData {
140 slow_rules: top_slow_rules(&report, 3),
141 ..report
142 }
143}
144
145pub fn apply_baseline(report: &mut ReportData, baseline: &ReportData) -> BaselineSummary {
146 let mut suppressed = 0;
147 let baseline_keys: HashSet<String> = baseline
148 .results
149 .iter()
150 .filter(|r| matches!(r.status, RuleStatus::Fail | RuleStatus::Error))
151 .map(finding_key)
152 .collect();
153
154 report.results.retain(|r| {
155 if !matches!(r.status, RuleStatus::Fail | RuleStatus::Error) {
156 return true;
157 }
158 let key = finding_key(r);
159 let keep = !baseline_keys.contains(&key);
160 if !keep {
161 suppressed += 1;
162 }
163 keep
164 });
165
166 BaselineSummary { suppressed }
167}
168
169pub fn should_exit_with_failure(report: &ReportData, fail_on: FailOn) -> bool {
170 match fail_on {
171 FailOn::Off => false,
172 FailOn::Error => report.results.iter().any(|item| {
173 matches!(item.status, RuleStatus::Fail | RuleStatus::Error)
174 && matches!(item.severity, Severity::Error)
175 }),
176 FailOn::Warning => report.results.iter().any(|item| {
177 matches!(item.status, RuleStatus::Fail | RuleStatus::Error)
178 && matches!(item.severity, Severity::Error | Severity::Warning)
179 }),
180 }
181}
182
183pub fn top_slow_rules(report: &ReportData, limit: usize) -> Vec<SlowRule> {
184 let mut items: Vec<SlowRule> = report
185 .results
186 .iter()
187 .map(|item| SlowRule {
188 rule_id: item.rule_id.clone(),
189 rule_name: item.rule_name.clone(),
190 duration_ms: item.duration_ms,
191 })
192 .collect();
193 items.sort_by(|a, b| {
194 b.duration_ms
195 .cmp(&a.duration_ms)
196 .then_with(|| a.rule_id.cmp(&b.rule_id))
197 });
198 items.truncate(limit);
199 items
200}
201
202fn finding_key(item: &ReportItem) -> String {
203 format!(
204 "{}|{}|{}",
205 item.rule_id,
206 item.target,
207 item.evidence.clone().unwrap_or_default()
208 )
209}