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}