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}