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}
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}