Skip to main content

allow_report/
diff_json.rs

1use allow_core::json_escape;
2
3use crate::DiffReport;
4use crate::diff_posture::{diff_evidence_delta_summary, diff_structural_delta_summary};
5use crate::explain_json::structural_identity_json;
6use crate::json::{json_string_array, option_json};
7use crate::{REPORT_COMMAND_DIFF, REPORT_SCHEMA_ID};
8
9pub fn render_diff_json_with_posture(report_json: &str, report: DiffReport<'_>) -> Option<String> {
10    if !is_diff_report_artifact(report_json) {
11        return None;
12    }
13    let diff_json = render_diff_posture_json(report);
14    let trimmed = report_json.trim_end();
15    trimmed
16        .strip_suffix('}')
17        .map(|prefix| format!("{prefix},\n  \"diff\": {diff_json}\n}}\n"))
18}
19
20fn is_diff_report_artifact(report_json: &str) -> bool {
21    contains_json_string_field(report_json, "schema_id", REPORT_SCHEMA_ID)
22        && contains_json_string_field(report_json, "command", REPORT_COMMAND_DIFF)
23}
24
25fn contains_json_string_field(json: &str, field: &str, value: &str) -> bool {
26    let spaced = format!("\"{field}\": \"{value}\"");
27    let compact = format!("\"{field}\":\"{value}\"");
28    json.contains(&spaced) || json.contains(&compact)
29}
30
31pub(crate) fn render_diff_posture_json(report: DiffReport<'_>) -> String {
32    render_diff_posture_json_with_evidence_health(report, 0, 0, 0)
33}
34
35pub(crate) fn render_diff_posture_json_with_evidence_health(
36    report: DiffReport<'_>,
37    broken_evidence_links: usize,
38    missing_evidence: usize,
39    weak_evidence_references: usize,
40) -> String {
41    let mut out = String::new();
42    out.push_str("{\n");
43    out.push_str(&format!(
44        "    \"net_posture\": \"{}\",\n",
45        json_escape(report.net_posture)
46    ));
47    out.push_str(&format!(
48        "    \"reviewer_action\": \"{}\",\n",
49        json_escape(report.reviewer_action)
50    ));
51    out.push_str("    \"summary\": {\n");
52    out.push_str(&format!(
53        "      \"current_failures\": {},\n",
54        report.summary.current_failures
55    ));
56    if broken_evidence_links > 0 {
57        out.push_str(&format!(
58            "      \"broken_evidence_links\": {},\n",
59            broken_evidence_links
60        ));
61    }
62    if missing_evidence > 0 {
63        out.push_str(&format!(
64            "      \"missing_evidence\": {},\n",
65            missing_evidence
66        ));
67    }
68    if weak_evidence_references > 0 {
69        out.push_str(&format!(
70            "      \"weak_evidence_references\": {},\n",
71            weak_evidence_references
72        ));
73    }
74    let structural_delta = diff_structural_delta_summary(report.policy_changes);
75    if structural_delta.scope_broadened > 0 {
76        out.push_str(&format!(
77            "      \"scope_broadened\": {},\n",
78            structural_delta.scope_broadened
79        ));
80    }
81    if structural_delta.scope_changed > 0 {
82        out.push_str(&format!(
83            "      \"scope_changed\": {},\n",
84            structural_delta.scope_changed
85        ));
86    }
87    if structural_delta.scope_narrowed > 0 {
88        out.push_str(&format!(
89            "      \"scope_narrowed\": {},\n",
90            structural_delta.scope_narrowed
91        ));
92    }
93    if structural_delta.selector_changed > 0 {
94        out.push_str(&format!(
95            "      \"selector_changed\": {},\n",
96            structural_delta.selector_changed
97        ));
98    }
99    if structural_delta.selector_precision_decreased > 0 {
100        out.push_str(&format!(
101            "      \"selector_precision_decreased\": {},\n",
102            structural_delta.selector_precision_decreased
103        ));
104    }
105    if structural_delta.selector_precision_increased > 0 {
106        out.push_str(&format!(
107            "      \"selector_precision_increased\": {},\n",
108            structural_delta.selector_precision_increased
109        ));
110    }
111    let evidence_delta = diff_evidence_delta_summary(report.policy_changes);
112    if evidence_delta.evidence_added > 0 {
113        out.push_str(&format!(
114            "      \"evidence_added\": {},\n",
115            evidence_delta.evidence_added
116        ));
117    }
118    if evidence_delta.weak_evidence_added > 0 {
119        out.push_str(&format!(
120            "      \"weak_evidence_added\": {},\n",
121            evidence_delta.weak_evidence_added
122        ));
123    }
124    if evidence_delta.broken_evidence_added > 0 {
125        out.push_str(&format!(
126            "      \"broken_evidence_added\": {},\n",
127            evidence_delta.broken_evidence_added
128        ));
129    }
130    if evidence_delta.evidence_removed > 0 {
131        out.push_str(&format!(
132            "      \"evidence_removed\": {},\n",
133            evidence_delta.evidence_removed
134        ));
135    }
136    if evidence_delta.evidence_removal_failures > 0 {
137        out.push_str(&format!(
138            "      \"evidence_removal_failures\": {},\n",
139            evidence_delta.evidence_removal_failures
140        ));
141    }
142    if evidence_delta.evidence_removal_review_items > 0 {
143        out.push_str(&format!(
144            "      \"evidence_removal_review_items\": {},\n",
145            evidence_delta.evidence_removal_review_items
146        ));
147    }
148    if evidence_delta.evidence_removal_improvements > 0 {
149        out.push_str(&format!(
150            "      \"evidence_removal_improvements\": {},\n",
151            evidence_delta.evidence_removal_improvements
152        ));
153    }
154    if evidence_delta.link_added > 0 {
155        out.push_str(&format!(
156            "      \"link_added\": {},\n",
157            evidence_delta.link_added
158        ));
159    }
160    if evidence_delta.weak_link_added > 0 {
161        out.push_str(&format!(
162            "      \"weak_link_added\": {},\n",
163            evidence_delta.weak_link_added
164        ));
165    }
166    if evidence_delta.broken_link_added > 0 {
167        out.push_str(&format!(
168            "      \"broken_link_added\": {},\n",
169            evidence_delta.broken_link_added
170        ));
171    }
172    if evidence_delta.link_removed > 0 {
173        out.push_str(&format!(
174            "      \"link_removed\": {},\n",
175            evidence_delta.link_removed
176        ));
177    }
178    if evidence_delta.link_removal_failures > 0 {
179        out.push_str(&format!(
180            "      \"link_removal_failures\": {},\n",
181            evidence_delta.link_removal_failures
182        ));
183    }
184    if evidence_delta.link_removal_review_items > 0 {
185        out.push_str(&format!(
186            "      \"link_removal_review_items\": {},\n",
187            evidence_delta.link_removal_review_items
188        ));
189    }
190    if evidence_delta.link_removal_improvements > 0 {
191        out.push_str(&format!(
192            "      \"link_removal_improvements\": {},\n",
193            evidence_delta.link_removal_improvements
194        ));
195    }
196    out.push_str(&format!(
197        "      \"new_findings\": {},\n",
198        report.summary.new_findings
199    ));
200    out.push_str(&format!(
201        "      \"removed_findings\": {},\n",
202        report.summary.removed_findings
203    ));
204    out.push_str(&format!(
205        "      \"policy_failures\": {},\n",
206        report.summary.policy_failures
207    ));
208    out.push_str(&format!(
209        "      \"policy_review_items\": {},\n",
210        report.summary.policy_review_items
211    ));
212    out.push_str(&format!(
213        "      \"policy_improvements\": {}\n",
214        report.summary.policy_improvements
215    ));
216    out.push_str("    },\n");
217    out.push_str("    \"finding_changes\": [\n");
218    for (index, change) in report.finding_changes.iter().enumerate() {
219        if index > 0 {
220            out.push_str(",\n");
221        }
222        out.push_str("      {");
223        out.push_str(&format!("\"change\": \"{}\", ", json_escape(change.change)));
224        out.push_str(&format!("\"key\": \"{}\", ", json_escape(change.key)));
225        out.push_str(&format!("\"kind\": \"{}\", ", json_escape(change.kind)));
226        out.push_str(&format!("\"family\": {}, ", option_json(change.family)));
227        out.push_str(&format!("\"path\": \"{}\"", json_escape(change.path)));
228        if let Some(line) = change.line {
229            out.push_str(&format!(", \"line\": {line}"));
230        }
231        if let Some(column) = change.column {
232            out.push_str(&format!(", \"column\": {column}"));
233        }
234        if let Some(source_package) = change.source_package {
235            out.push_str(&format!(
236                ", \"source_package\": \"{}\"",
237                json_escape(source_package)
238            ));
239        }
240        if let Some(identity) = change.identity {
241            out.push_str(", \"identity\": ");
242            out.push_str(&structural_identity_json(identity, "    "));
243        }
244        out.push('}');
245    }
246    out.push_str("\n    ],\n");
247    out.push_str("    \"policy_changes\": [\n");
248    for (index, change) in report.policy_changes.iter().enumerate() {
249        if index > 0 {
250            out.push_str(",\n");
251        }
252        out.push_str("      {");
253        out.push_str(&format!(
254            "\"severity\": \"{}\", ",
255            json_escape(change.severity)
256        ));
257        out.push_str(&format!(
258            "\"allow_id\": \"{}\", ",
259            json_escape(change.allow_id)
260        ));
261        out.push_str(&format!("\"kind\": \"{}\", ", json_escape(change.kind)));
262        out.push_str(&format!("\"message\": \"{}\"", json_escape(change.message)));
263        if let Some(exception_identity) = change.exception_identity {
264            out.push_str(", ");
265            out.push_str(&format!(
266                "\"exception_identity\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
267                json_escape(exception_identity.field),
268                option_json(exception_identity.before),
269                option_json(exception_identity.after)
270            ));
271        }
272        if let Some(selector_identity) = change.selector_identity {
273            out.push_str(", ");
274            out.push_str(&format!(
275                "\"selector_identity\": {{\"changed_fields\": {}}}",
276                json_string_array(selector_identity.changed_fields)
277            ));
278        }
279        if let Some(selector_precision) = change.selector_precision {
280            out.push_str(", ");
281            out.push_str(&format!(
282                "\"selector_precision\": {{\"before\": {}, \"after\": {}, \"removed_fields\": {}, \"added_fields\": {}}}",
283                selector_precision.before,
284                selector_precision.after,
285                json_string_array(selector_precision.removed_fields),
286                json_string_array(selector_precision.added_fields)
287            ));
288        }
289        if let Some(scope) = change.scope {
290            out.push_str(", ");
291            out.push_str(&format!(
292                "\"scope\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
293                json_escape(scope.field),
294                option_json(scope.before),
295                option_json(scope.after)
296            ));
297        }
298        if let Some(limit) = change.occurrence_limit {
299            out.push_str(", ");
300            out.push_str(&format!(
301                "\"occurrence_limit\": {{\"before\": {}, \"after\": {}}}",
302                option_u32_json(limit.before),
303                option_u32_json(limit.after)
304            ));
305        }
306        if let Some(lifecycle) = change.lifecycle {
307            out.push_str(", ");
308            out.push_str(&format!(
309                "\"lifecycle\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
310                json_escape(lifecycle.field),
311                option_json(lifecycle.before),
312                option_json(lifecycle.after)
313            ));
314        }
315        if let Some(evidence) = change.evidence {
316            out.push_str(", ");
317            out.push_str(&format!(
318                "\"evidence\": {{\"field\": \"{}\", \"removed\": {}, \"added\": {}}}",
319                json_escape(evidence.field),
320                json_string_array(evidence.removed),
321                json_string_array(evidence.added)
322            ));
323        }
324        if let Some(metadata) = change.metadata {
325            out.push_str(", ");
326            out.push_str(&format!(
327                "\"metadata\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
328                json_escape(metadata.field),
329                option_json(metadata.before),
330                option_json(metadata.after)
331            ));
332        }
333        if let Some(requirement) = change.requirement {
334            out.push_str(", ");
335            out.push_str(&format!(
336                "\"requirement\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
337                json_escape(requirement.field),
338                requirement.before,
339                requirement.after
340            ));
341        }
342        if let Some(policy_status) = change.policy_status {
343            out.push_str(", ");
344            out.push_str(&format!(
345                "\"policy_status\": {{\"before\": {}, \"after\": {}}}",
346                option_json(policy_status.before),
347                option_json(policy_status.after)
348            ));
349        }
350        out.push('}');
351    }
352    out.push_str("\n    ]\n");
353    out.push_str("  }");
354    out
355}
356
357fn option_u32_json(value: Option<u32>) -> String {
358    value.map_or_else(|| "null".to_string(), |value| value.to_string())
359}