Skip to main content

allow_report/
diff_json.rs

1use allow_core::json_escape;
2
3use crate::DiffReport;
4use crate::json::{json_string_array, option_json};
5use crate::{REPORT_COMMAND_DIFF, REPORT_SCHEMA_ID};
6
7pub fn render_diff_json_with_posture(report_json: &str, report: DiffReport<'_>) -> Option<String> {
8    if !is_diff_report_artifact(report_json) {
9        return None;
10    }
11    let diff_json = render_diff_posture_json(report);
12    let trimmed = report_json.trim_end();
13    trimmed
14        .strip_suffix('}')
15        .map(|prefix| format!("{prefix},\n  \"diff\": {diff_json}\n}}\n"))
16}
17
18fn is_diff_report_artifact(report_json: &str) -> bool {
19    contains_json_string_field(report_json, "schema_id", REPORT_SCHEMA_ID)
20        && contains_json_string_field(report_json, "command", REPORT_COMMAND_DIFF)
21}
22
23fn contains_json_string_field(json: &str, field: &str, value: &str) -> bool {
24    let spaced = format!("\"{field}\": \"{value}\"");
25    let compact = format!("\"{field}\":\"{value}\"");
26    json.contains(&spaced) || json.contains(&compact)
27}
28
29pub(crate) fn render_diff_posture_json(report: DiffReport<'_>) -> String {
30    render_diff_posture_json_with_evidence_health(report, 0, 0)
31}
32
33pub(crate) fn render_diff_posture_json_with_evidence_health(
34    report: DiffReport<'_>,
35    broken_evidence_links: usize,
36    weak_evidence_references: usize,
37) -> String {
38    let mut out = String::new();
39    out.push_str("{\n");
40    out.push_str(&format!(
41        "    \"net_posture\": \"{}\",\n",
42        json_escape(report.net_posture)
43    ));
44    out.push_str(&format!(
45        "    \"reviewer_action\": \"{}\",\n",
46        json_escape(report.reviewer_action)
47    ));
48    out.push_str("    \"summary\": {\n");
49    out.push_str(&format!(
50        "      \"current_failures\": {},\n",
51        report.summary.current_failures
52    ));
53    if broken_evidence_links > 0 {
54        out.push_str(&format!(
55            "      \"broken_evidence_links\": {},\n",
56            broken_evidence_links
57        ));
58    }
59    if weak_evidence_references > 0 {
60        out.push_str(&format!(
61            "      \"weak_evidence_references\": {},\n",
62            weak_evidence_references
63        ));
64    }
65    out.push_str(&format!(
66        "      \"new_findings\": {},\n",
67        report.summary.new_findings
68    ));
69    out.push_str(&format!(
70        "      \"removed_findings\": {},\n",
71        report.summary.removed_findings
72    ));
73    out.push_str(&format!(
74        "      \"policy_failures\": {},\n",
75        report.summary.policy_failures
76    ));
77    out.push_str(&format!(
78        "      \"policy_review_items\": {},\n",
79        report.summary.policy_review_items
80    ));
81    out.push_str(&format!(
82        "      \"policy_improvements\": {}\n",
83        report.summary.policy_improvements
84    ));
85    out.push_str("    },\n");
86    out.push_str("    \"finding_changes\": [\n");
87    for (index, change) in report.finding_changes.iter().enumerate() {
88        if index > 0 {
89            out.push_str(",\n");
90        }
91        out.push_str("      {");
92        out.push_str(&format!("\"change\": \"{}\", ", json_escape(change.change)));
93        out.push_str(&format!("\"key\": \"{}\", ", json_escape(change.key)));
94        out.push_str(&format!("\"kind\": \"{}\", ", json_escape(change.kind)));
95        out.push_str(&format!("\"family\": {}, ", option_json(change.family)));
96        out.push_str(&format!("\"path\": \"{}\"", json_escape(change.path)));
97        out.push('}');
98    }
99    out.push_str("\n    ],\n");
100    out.push_str("    \"policy_changes\": [\n");
101    for (index, change) in report.policy_changes.iter().enumerate() {
102        if index > 0 {
103            out.push_str(",\n");
104        }
105        out.push_str("      {");
106        out.push_str(&format!(
107            "\"severity\": \"{}\", ",
108            json_escape(change.severity)
109        ));
110        out.push_str(&format!(
111            "\"allow_id\": \"{}\", ",
112            json_escape(change.allow_id)
113        ));
114        out.push_str(&format!("\"kind\": \"{}\", ", json_escape(change.kind)));
115        out.push_str(&format!("\"message\": \"{}\"", json_escape(change.message)));
116        if let Some(exception_identity) = change.exception_identity {
117            out.push_str(", ");
118            out.push_str(&format!(
119                "\"exception_identity\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
120                json_escape(exception_identity.field),
121                option_json(exception_identity.before),
122                option_json(exception_identity.after)
123            ));
124        }
125        if let Some(selector_identity) = change.selector_identity {
126            out.push_str(", ");
127            out.push_str(&format!(
128                "\"selector_identity\": {{\"changed_fields\": {}}}",
129                json_string_array(selector_identity.changed_fields)
130            ));
131        }
132        if let Some(selector_precision) = change.selector_precision {
133            out.push_str(", ");
134            out.push_str(&format!(
135                "\"selector_precision\": {{\"before\": {}, \"after\": {}, \"removed_fields\": {}, \"added_fields\": {}}}",
136                selector_precision.before,
137                selector_precision.after,
138                json_string_array(selector_precision.removed_fields),
139                json_string_array(selector_precision.added_fields)
140            ));
141        }
142        if let Some(scope) = change.scope {
143            out.push_str(", ");
144            out.push_str(&format!(
145                "\"scope\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
146                json_escape(scope.field),
147                option_json(scope.before),
148                option_json(scope.after)
149            ));
150        }
151        if let Some(limit) = change.occurrence_limit {
152            out.push_str(", ");
153            out.push_str(&format!(
154                "\"occurrence_limit\": {{\"before\": {}, \"after\": {}}}",
155                option_u32_json(limit.before),
156                option_u32_json(limit.after)
157            ));
158        }
159        if let Some(lifecycle) = change.lifecycle {
160            out.push_str(", ");
161            out.push_str(&format!(
162                "\"lifecycle\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
163                json_escape(lifecycle.field),
164                option_json(lifecycle.before),
165                option_json(lifecycle.after)
166            ));
167        }
168        if let Some(evidence) = change.evidence {
169            out.push_str(", ");
170            out.push_str(&format!(
171                "\"evidence\": {{\"field\": \"{}\", \"removed\": {}, \"added\": {}}}",
172                json_escape(evidence.field),
173                json_string_array(evidence.removed),
174                json_string_array(evidence.added)
175            ));
176        }
177        if let Some(metadata) = change.metadata {
178            out.push_str(", ");
179            out.push_str(&format!(
180                "\"metadata\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
181                json_escape(metadata.field),
182                option_json(metadata.before),
183                option_json(metadata.after)
184            ));
185        }
186        if let Some(requirement) = change.requirement {
187            out.push_str(", ");
188            out.push_str(&format!(
189                "\"requirement\": {{\"field\": \"{}\", \"before\": {}, \"after\": {}}}",
190                json_escape(requirement.field),
191                requirement.before,
192                requirement.after
193            ));
194        }
195        if let Some(policy_status) = change.policy_status {
196            out.push_str(", ");
197            out.push_str(&format!(
198                "\"policy_status\": {{\"before\": {}, \"after\": {}}}",
199                option_json(policy_status.before),
200                option_json(policy_status.after)
201            ));
202        }
203        out.push('}');
204    }
205    out.push_str("\n    ]\n");
206    out.push_str("  }");
207    out
208}
209
210fn option_u32_json(value: Option<u32>) -> String {
211    value.map_or_else(|| "null".to_string(), |value| value.to_string())
212}