Skip to main content

libverify_output/
lib.rs

1pub mod json;
2pub mod matrix;
3pub mod sarif;
4pub mod vanta;
5
6pub use sarif::utc_now_rfc3339;
7
8use anyhow::Result;
9use libverify_core::assessment::{BatchReport, VerificationResult};
10
11#[derive(Debug, Clone, Copy)]
12pub enum Format {
13    Json,
14    Matrix,
15    Sarif,
16    Vanta,
17}
18
19pub struct OutputOptions {
20    pub format: Format,
21    pub only_failures: bool,
22    /// Tool name for SARIF output (e.g. "gh-verify", "atlassian-verify").
23    pub tool_name: String,
24    /// Tool version for SARIF output.
25    pub tool_version: String,
26}
27
28pub fn parse_format(s: &str) -> Result<Format> {
29    match s {
30        "json" => Ok(Format::Json),
31        "matrix" => Ok(Format::Matrix),
32        "sarif" => Ok(Format::Sarif),
33        "vanta" => Ok(Format::Vanta),
34        _ => anyhow::bail!("invalid format: {s} (use 'json', 'matrix', 'sarif', or 'vanta')"),
35    }
36}
37
38pub fn render(opts: &OutputOptions, result: &VerificationResult) -> Result<String> {
39    match opts.format {
40        Format::Json => json::render(result, opts.only_failures),
41        Format::Matrix => matrix::render(result, opts.only_failures),
42        Format::Sarif => sarif::render(
43            result,
44            opts.only_failures,
45            &opts.tool_name,
46            &opts.tool_version,
47        ),
48        Format::Vanta => vanta::render(result, opts.only_failures),
49    }
50}
51
52pub fn render_batch(opts: &OutputOptions, batch: &BatchReport) -> Result<String> {
53    match opts.format {
54        Format::Json => json::render_batch(batch, opts.only_failures),
55        Format::Matrix => matrix::render_batch(batch, opts.only_failures),
56        Format::Sarif => sarif::render_batch(
57            batch,
58            opts.only_failures,
59            &opts.tool_name,
60            &opts.tool_version,
61        ),
62        Format::Vanta => vanta::render_batch(batch, opts.only_failures),
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use libverify_core::assessment::AssessmentReport;
70    use libverify_core::control::{ControlFinding, builtin};
71    use libverify_core::profile::{FindingSeverity, GateDecision, ProfileOutcome};
72
73    fn opts(format: Format) -> OutputOptions {
74        OutputOptions {
75            format,
76            only_failures: false,
77            tool_name: "test".to_string(),
78            tool_version: "0.1".to_string(),
79        }
80    }
81
82    fn sample_result() -> VerificationResult {
83        VerificationResult {
84            report: AssessmentReport {
85                profile_name: "test".to_string(),
86                findings: vec![ControlFinding::satisfied(
87                    builtin::id(builtin::REVIEW_INDEPENDENCE),
88                    "ok",
89                    vec![],
90                )],
91                outcomes: vec![ProfileOutcome {
92                    control_id: builtin::id(builtin::REVIEW_INDEPENDENCE),
93                    severity: FindingSeverity::Info,
94                    decision: GateDecision::Pass,
95                    rationale: "ok".to_string(),
96                    annotations: Default::default(),
97                }],
98                severity_labels: Default::default(),
99            },
100            evidence: None,
101        }
102    }
103
104    #[test]
105    fn parse_format_valid() {
106        assert!(matches!(parse_format("json").unwrap(), Format::Json));
107        assert!(matches!(parse_format("matrix").unwrap(), Format::Matrix));
108        assert!(matches!(parse_format("sarif").unwrap(), Format::Sarif));
109        assert!(matches!(parse_format("vanta").unwrap(), Format::Vanta));
110    }
111
112    #[test]
113    fn parse_format_invalid() {
114        assert!(parse_format("xml").is_err());
115        assert!(parse_format("").is_err());
116    }
117
118    #[test]
119    fn render_json_format() {
120        let output = render(&opts(Format::Json), &sample_result()).unwrap();
121        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
122        assert_eq!(parsed["profile_name"], "test");
123    }
124
125    #[test]
126    fn render_sarif_format() {
127        let output = render(&opts(Format::Sarif), &sample_result()).unwrap();
128        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
129        assert_eq!(parsed["version"], "2.1.0");
130    }
131
132    #[test]
133    fn render_batch_json_format() {
134        let batch = BatchReport {
135            reports: vec![],
136            total_pass: 0,
137            total_review: 0,
138            total_fail: 0,
139            skipped: vec![],
140        };
141        let output = render_batch(&opts(Format::Json), &batch).unwrap();
142        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
143        assert!(parsed["reports"].as_array().unwrap().is_empty());
144    }
145
146    #[test]
147    fn render_batch_sarif_format() {
148        let batch = BatchReport {
149            reports: vec![],
150            total_pass: 0,
151            total_review: 0,
152            total_fail: 0,
153            skipped: vec![],
154        };
155        let output = render_batch(&opts(Format::Sarif), &batch).unwrap();
156        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
157        assert_eq!(parsed["version"], "2.1.0");
158    }
159}