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}