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