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