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