Skip to main content

verifyos_cli/report/
data.rs

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}