Skip to main content

cc_audit/output/
formatter.rs

1//! Output formatter for scan results.
2
3use crate::cli::OutputFormat;
4use crate::reporter::{
5    Reporter, html::HtmlReporter, json::JsonReporter, markdown::MarkdownReporter,
6    sarif::SarifReporter, terminal::TerminalReporter,
7};
8use crate::rules::ScanResult;
9
10/// Unified output formatter that selects the appropriate reporter.
11pub struct OutputFormatter {
12    format: OutputFormat,
13    strict: bool,
14    verbose: bool,
15}
16
17impl OutputFormatter {
18    /// Create a new output formatter.
19    pub fn new(format: OutputFormat) -> Self {
20        Self {
21            format,
22            strict: false,
23            verbose: false,
24        }
25    }
26
27    /// Set strict mode.
28    pub fn with_strict(mut self, strict: bool) -> Self {
29        self.strict = strict;
30        self
31    }
32
33    /// Set verbose output mode.
34    pub fn with_verbose(mut self, verbose: bool) -> Self {
35        self.verbose = verbose;
36        self
37    }
38
39    /// Format the scan result to a string.
40    pub fn format(&self, result: &ScanResult) -> String {
41        match self.format {
42            OutputFormat::Terminal => {
43                let reporter = TerminalReporter::new(self.strict, self.verbose);
44                reporter.report(result)
45            }
46            OutputFormat::Json => {
47                let reporter = JsonReporter::new();
48                reporter.report(result)
49            }
50            OutputFormat::Sarif => {
51                let reporter = SarifReporter::new();
52                reporter.report(result)
53            }
54            OutputFormat::Html => {
55                let reporter = HtmlReporter::new();
56                reporter.report(result)
57            }
58            OutputFormat::Markdown => {
59                let reporter = MarkdownReporter::new();
60                reporter.report(result)
61            }
62        }
63    }
64
65    /// Get the appropriate file extension for the output format.
66    pub fn extension(&self) -> &'static str {
67        match self.format {
68            OutputFormat::Terminal => "txt",
69            OutputFormat::Json => "json",
70            OutputFormat::Sarif => "sarif",
71            OutputFormat::Html => "html",
72            OutputFormat::Markdown => "md",
73        }
74    }
75}
76
77impl Default for OutputFormatter {
78    fn default() -> Self {
79        Self::new(OutputFormat::Terminal)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_formatter_extensions() {
89        assert_eq!(
90            OutputFormatter::new(OutputFormat::Terminal).extension(),
91            "txt"
92        );
93        assert_eq!(OutputFormatter::new(OutputFormat::Json).extension(), "json");
94        assert_eq!(
95            OutputFormatter::new(OutputFormat::Sarif).extension(),
96            "sarif"
97        );
98        assert_eq!(OutputFormatter::new(OutputFormat::Html).extension(), "html");
99        assert_eq!(
100            OutputFormatter::new(OutputFormat::Markdown).extension(),
101            "md"
102        );
103    }
104
105    #[test]
106    fn test_formatter_builder() {
107        let formatter = OutputFormatter::new(OutputFormat::Terminal)
108            .with_strict(true)
109            .with_verbose(true);
110
111        assert!(formatter.strict);
112        assert!(formatter.verbose);
113    }
114
115    #[test]
116    fn test_formatter_default() {
117        let formatter = OutputFormatter::default();
118        assert_eq!(formatter.format, OutputFormat::Terminal);
119        assert!(!formatter.strict);
120        assert!(!formatter.verbose);
121    }
122
123    fn create_test_result() -> crate::rules::ScanResult {
124        crate::rules::ScanResult {
125            version: "1.0.0".to_string(),
126            scanned_at: "2024-01-01T00:00:00Z".to_string(),
127            target: "/test".to_string(),
128            summary: crate::rules::Summary {
129                critical: 0,
130                high: 0,
131                medium: 0,
132                low: 0,
133                passed: true,
134                errors: 0,
135                warnings: 0,
136            },
137            findings: Vec::new(),
138            risk_score: None,
139            elapsed_ms: 0,
140        }
141    }
142
143    #[test]
144    fn test_formatter_format_terminal() {
145        let formatter = OutputFormatter::new(OutputFormat::Terminal);
146        let result = create_test_result();
147        let output = formatter.format(&result);
148        assert!(
149            output.contains("Pass")
150                || output.contains("pass")
151                || output.contains("PASS")
152                || output.contains("No findings")
153                || !output.is_empty()
154        );
155    }
156
157    #[test]
158    fn test_formatter_format_json() {
159        let formatter = OutputFormatter::new(OutputFormat::Json);
160        let result = create_test_result();
161        let output = formatter.format(&result);
162        assert!(output.starts_with('{'));
163        assert!(output.ends_with('}'));
164    }
165
166    #[test]
167    fn test_formatter_format_sarif() {
168        let formatter = OutputFormatter::new(OutputFormat::Sarif);
169        let result = create_test_result();
170        let output = formatter.format(&result);
171        assert!(output.contains("sarif") || output.contains("$schema"));
172    }
173
174    #[test]
175    fn test_formatter_format_html() {
176        let formatter = OutputFormatter::new(OutputFormat::Html);
177        let result = create_test_result();
178        let output = formatter.format(&result);
179        assert!(output.contains("<html>") || output.contains("<!DOCTYPE"));
180    }
181
182    #[test]
183    fn test_formatter_format_markdown() {
184        let formatter = OutputFormatter::new(OutputFormat::Markdown);
185        let result = create_test_result();
186        let output = formatter.format(&result);
187        assert!(output.contains('#') || output.contains("##"));
188    }
189}