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        }
140    }
141
142    #[test]
143    fn test_formatter_format_terminal() {
144        let formatter = OutputFormatter::new(OutputFormat::Terminal);
145        let result = create_test_result();
146        let output = formatter.format(&result);
147        assert!(
148            output.contains("Pass")
149                || output.contains("pass")
150                || output.contains("PASS")
151                || output.contains("No findings")
152                || !output.is_empty()
153        );
154    }
155
156    #[test]
157    fn test_formatter_format_json() {
158        let formatter = OutputFormatter::new(OutputFormat::Json);
159        let result = create_test_result();
160        let output = formatter.format(&result);
161        assert!(output.starts_with('{'));
162        assert!(output.ends_with('}'));
163    }
164
165    #[test]
166    fn test_formatter_format_sarif() {
167        let formatter = OutputFormatter::new(OutputFormat::Sarif);
168        let result = create_test_result();
169        let output = formatter.format(&result);
170        assert!(output.contains("sarif") || output.contains("$schema"));
171    }
172
173    #[test]
174    fn test_formatter_format_html() {
175        let formatter = OutputFormatter::new(OutputFormat::Html);
176        let result = create_test_result();
177        let output = formatter.format(&result);
178        assert!(output.contains("<html>") || output.contains("<!DOCTYPE"));
179    }
180
181    #[test]
182    fn test_formatter_format_markdown() {
183        let formatter = OutputFormatter::new(OutputFormat::Markdown);
184        let result = create_test_result();
185        let output = formatter.format(&result);
186        assert!(output.contains('#') || output.contains("##"));
187    }
188}