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}