Skip to main content

cc_audit/aggregator/
summary.rs

1//! Summary builder for scan results.
2
3use crate::rules::{Finding, Severity, Summary};
4use rustc_hash::FxHashMap;
5
6/// Builder for creating scan summaries.
7#[derive(Debug, Default)]
8pub struct SummaryBuilder {
9    findings: Vec<Finding>,
10    files_scanned: usize,
11    scan_duration_ms: u64,
12}
13
14impl SummaryBuilder {
15    /// Create a new summary builder.
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Add findings to the summary.
21    pub fn with_findings(mut self, findings: Vec<Finding>) -> Self {
22        self.findings = findings;
23        self
24    }
25
26    /// Set the number of files scanned.
27    pub fn with_files_scanned(mut self, count: usize) -> Self {
28        self.files_scanned = count;
29        self
30    }
31
32    /// Set the scan duration in milliseconds.
33    pub fn with_duration_ms(mut self, duration: u64) -> Self {
34        self.scan_duration_ms = duration;
35        self
36    }
37
38    /// Build the summary.
39    pub fn build(self) -> Summary {
40        let mut by_severity: FxHashMap<Severity, usize> = FxHashMap::default();
41
42        for finding in &self.findings {
43            *by_severity.entry(finding.severity).or_default() += 1;
44        }
45
46        let critical = by_severity.get(&Severity::Critical).copied().unwrap_or(0);
47        let high = by_severity.get(&Severity::High).copied().unwrap_or(0);
48        let medium = by_severity.get(&Severity::Medium).copied().unwrap_or(0);
49        let low = by_severity.get(&Severity::Low).copied().unwrap_or(0);
50
51        Summary {
52            critical,
53            high,
54            medium,
55            low,
56            passed: critical == 0 && high == 0,
57            errors: 0,
58            warnings: 0,
59        }
60    }
61
62    /// Get the number of files scanned.
63    pub fn files_scanned(&self) -> usize {
64        self.files_scanned
65    }
66
67    /// Get the total number of findings.
68    pub fn total_findings(&self) -> usize {
69        self.findings.len()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::rules::{Category, Confidence, Location};
77
78    fn make_finding(severity: Severity) -> Finding {
79        Finding {
80            id: "TEST-001".to_string(),
81            severity,
82            category: Category::PromptInjection,
83            confidence: Confidence::Firm,
84            name: "Test".to_string(),
85            location: Location {
86                file: "test.md".to_string(),
87                line: 1,
88                column: None,
89            },
90            code: "test".to_string(),
91            message: "test".to_string(),
92            recommendation: "fix".to_string(),
93            fix_hint: None,
94            cwe_ids: Vec::new(),
95            rule_severity: None,
96            client: None,
97            context: None,
98        }
99    }
100
101    #[test]
102    fn test_summary_builder() {
103        let findings = vec![
104            make_finding(Severity::Critical),
105            make_finding(Severity::High),
106            make_finding(Severity::High),
107            make_finding(Severity::Medium),
108        ];
109
110        let builder = SummaryBuilder::new()
111            .with_findings(findings)
112            .with_files_scanned(10);
113
114        assert_eq!(builder.total_findings(), 4);
115        assert_eq!(builder.files_scanned(), 10);
116
117        let summary = builder.build();
118        assert_eq!(summary.critical, 1);
119        assert_eq!(summary.high, 2);
120        assert_eq!(summary.medium, 1);
121        assert_eq!(summary.low, 0);
122        assert!(!summary.passed); // Has critical finding
123    }
124
125    #[test]
126    fn test_empty_summary() {
127        let builder = SummaryBuilder::new().with_files_scanned(5);
128
129        assert_eq!(builder.total_findings(), 0);
130        assert_eq!(builder.files_scanned(), 5);
131
132        let summary = builder.build();
133        assert_eq!(summary.critical, 0);
134        assert!(summary.passed); // No findings
135    }
136}