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