Skip to main content

libverify_output/
json.rs

1use anyhow::Result;
2use libverify_core::assessment::{AssessmentReport, BatchEntry, BatchReport, VerificationResult};
3use libverify_core::profile::GateDecision;
4
5pub fn render(result: &VerificationResult, only_failures: bool) -> Result<String> {
6    if only_failures {
7        let filtered = filter_result(result);
8        Ok(serde_json::to_string_pretty(&filtered)?)
9    } else {
10        Ok(serde_json::to_string_pretty(result)?)
11    }
12}
13
14pub fn render_batch(batch: &BatchReport, only_failures: bool) -> Result<String> {
15    if only_failures {
16        let filtered = filter_batch(batch);
17        Ok(serde_json::to_string_pretty(&filtered)?)
18    } else {
19        Ok(serde_json::to_string_pretty(batch)?)
20    }
21}
22
23fn filter_result(result: &VerificationResult) -> VerificationResult {
24    let report = &result.report;
25    let mut filtered_findings = Vec::new();
26    let mut filtered_outcomes = Vec::new();
27
28    for (finding, outcome) in report.findings.iter().zip(report.outcomes.iter()) {
29        if outcome.decision == GateDecision::Fail {
30            filtered_findings.push(finding.clone());
31            filtered_outcomes.push(outcome.clone());
32        }
33    }
34
35    VerificationResult {
36        report: AssessmentReport {
37            profile_name: report.profile_name.clone(),
38            findings: filtered_findings,
39            outcomes: filtered_outcomes,
40            severity_labels: report.severity_labels.clone(),
41        },
42        evidence: result.evidence.clone(),
43    }
44}
45
46fn filter_batch(batch: &BatchReport) -> BatchReport {
47    BatchReport {
48        reports: batch
49            .reports
50            .iter()
51            .map(|entry| BatchEntry {
52                subject_id: entry.subject_id.clone(),
53                result: filter_result(&entry.result),
54            })
55            .collect(),
56        total_pass: batch.total_pass,
57        total_review: batch.total_review,
58        total_fail: batch.total_fail,
59        skipped: batch.skipped.clone(),
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use libverify_core::assessment::AssessmentReport;
67    use libverify_core::control::{ControlFinding, builtin};
68    use libverify_core::profile::{FindingSeverity, ProfileOutcome};
69
70    fn sample_result() -> VerificationResult {
71        VerificationResult {
72            report: AssessmentReport {
73                profile_name: "test".to_string(),
74                findings: vec![
75                    ControlFinding::satisfied(
76                        builtin::id(builtin::REVIEW_INDEPENDENCE),
77                        "approved",
78                        vec!["pr:1".to_string()],
79                    ),
80                    ControlFinding::violated(
81                        builtin::id(builtin::SOURCE_AUTHENTICITY),
82                        "unsigned",
83                        vec!["pr:1".to_string()],
84                    ),
85                ],
86                outcomes: vec![
87                    ProfileOutcome {
88                        control_id: builtin::id(builtin::REVIEW_INDEPENDENCE),
89                        severity: FindingSeverity::Info,
90                        decision: GateDecision::Pass,
91                        rationale: "approved".to_string(),
92                        annotations: Default::default(),
93                    },
94                    ProfileOutcome {
95                        control_id: builtin::id(builtin::SOURCE_AUTHENTICITY),
96                        severity: FindingSeverity::Error,
97                        decision: GateDecision::Fail,
98                        rationale: "unsigned".to_string(),
99                        annotations: Default::default(),
100                    },
101                ],
102                severity_labels: Default::default(),
103            },
104            evidence: None,
105        }
106    }
107
108    #[test]
109    fn render_produces_valid_json() {
110        let output = render(&sample_result(), false).unwrap();
111        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
112        assert_eq!(parsed["profile_name"], "test");
113        assert_eq!(parsed["findings"].as_array().unwrap().len(), 2);
114    }
115
116    #[test]
117    fn render_with_only_failures_filters_to_fail_only() {
118        let output = render(&sample_result(), true).unwrap();
119        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
120        let findings = parsed["findings"].as_array().unwrap();
121        assert_eq!(findings.len(), 1);
122        let outcomes = parsed["outcomes"].as_array().unwrap();
123        assert_eq!(outcomes.len(), 1);
124        assert_eq!(outcomes[0]["decision"], "fail");
125    }
126
127    #[test]
128    fn render_batch_produces_valid_json() {
129        let batch = BatchReport {
130            reports: vec![BatchEntry {
131                subject_id: "owner/repo".to_string(),
132                result: sample_result(),
133            }],
134            total_pass: 1,
135            total_review: 0,
136            total_fail: 1,
137            skipped: vec![],
138        };
139        let output = render_batch(&batch, false).unwrap();
140        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
141        assert_eq!(parsed["reports"].as_array().unwrap().len(), 1);
142    }
143
144    #[test]
145    fn render_batch_with_only_failures_filters() {
146        let batch = BatchReport {
147            reports: vec![BatchEntry {
148                subject_id: "owner/repo".to_string(),
149                result: sample_result(),
150            }],
151            total_pass: 1,
152            total_review: 0,
153            total_fail: 1,
154            skipped: vec![],
155        };
156        let output = render_batch(&batch, true).unwrap();
157        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
158        let outcomes = parsed["reports"][0]["outcomes"].as_array().unwrap();
159        assert_eq!(outcomes.len(), 1);
160        assert_eq!(outcomes[0]["decision"], "fail");
161    }
162
163    #[test]
164    fn filter_result_keeps_only_fail_decisions() {
165        let filtered = filter_result(&sample_result());
166        assert_eq!(filtered.report.findings.len(), 1);
167        assert_eq!(filtered.report.outcomes.len(), 1);
168        assert_eq!(filtered.report.outcomes[0].decision, GateDecision::Fail);
169    }
170
171    #[test]
172    fn filter_result_excludes_pass_and_review() {
173        let filtered = filter_result(&sample_result());
174        for outcome in &filtered.report.outcomes {
175            assert_eq!(outcome.decision, GateDecision::Fail);
176        }
177    }
178
179    #[test]
180    fn filter_batch_applies_filter_to_all_entries() {
181        let batch = BatchReport {
182            reports: vec![
183                BatchEntry {
184                    subject_id: "repo1".to_string(),
185                    result: sample_result(),
186                },
187                BatchEntry {
188                    subject_id: "repo2".to_string(),
189                    result: sample_result(),
190                },
191            ],
192            total_pass: 2,
193            total_review: 0,
194            total_fail: 2,
195            skipped: vec![],
196        };
197        let filtered = filter_batch(&batch);
198        assert_eq!(filtered.reports.len(), 2);
199        for entry in &filtered.reports {
200            assert_eq!(entry.result.report.outcomes.len(), 1);
201            assert_eq!(entry.result.report.outcomes[0].decision, GateDecision::Fail);
202        }
203    }
204}