allow_report/
diff_markdown.rs1use crate::diff_policy_detail::policy_change_detail;
2use crate::diff_posture::{diff_net_posture, diff_posture_summary};
3use crate::text::markdown_cell;
4use crate::{CLAIM_BOUNDARY_TEXT, DiffFindingChange, DiffPolicyChange};
5
6const PR_SUMMARY_HIGHLIGHT_LIMIT: usize = 8;
7
8pub fn render_diff_pr_summary_markdown(
9 current_failures: usize,
10 finding_changes: &[DiffFindingChange<'_>],
11 policy_changes: &[DiffPolicyChange<'_>],
12) -> String {
13 let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
14 let posture = diff_net_posture(summary);
15 let mut out = String::new();
16 out.push_str("## PR Summary\n\n");
17 out.push_str(&format!("**Net posture:** `{}`\n\n", posture.as_str()));
18 out.push_str("| Signal | Count |\n|---|---:|\n");
19 out.push_str(&format!(
20 "| Current check failures | {} |\n",
21 summary.current_failures
22 ));
23 out.push_str(&format!(
24 "| New source findings | {} |\n",
25 summary.new_findings
26 ));
27 out.push_str(&format!(
28 "| Removed source findings | {} |\n",
29 summary.removed_findings
30 ));
31 out.push_str(&format!(
32 "| Policy failures | {} |\n",
33 summary.policy_failures
34 ));
35 out.push_str(&format!(
36 "| Policy review items | {} |\n",
37 summary.policy_review_items
38 ));
39 out.push_str(&format!(
40 "| Policy improvements | {} |\n",
41 summary.policy_improvements
42 ));
43 out.push_str(&format!(
44 "\n**Reviewer action:** {}\n\n",
45 posture.reviewer_action()
46 ));
47 out.push_str("> ");
48 out.push_str(CLAIM_BOUNDARY_TEXT);
49 out.push_str("\n\n");
50 append_finding_highlights(&mut out, finding_changes);
51 append_policy_highlights(&mut out, policy_changes);
52 out
53}
54
55fn append_finding_highlights(out: &mut String, finding_changes: &[DiffFindingChange<'_>]) {
56 let new_count = finding_changes
57 .iter()
58 .filter(|change| change.change == "new")
59 .count();
60 if new_count > 0 {
61 out.push_str("### Finding Attention\n\n");
62 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
63 for change in finding_changes
64 .iter()
65 .filter(|change| change.change == "new")
66 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
67 {
68 append_finding_highlight_row(out, change);
69 }
70 append_omitted_summary_note(out, new_count, "new finding change");
71 out.push('\n');
72 }
73
74 let removed_count = finding_changes
75 .iter()
76 .filter(|change| change.change == "removed")
77 .count();
78 if removed_count > 0 {
79 out.push_str("### Finding Improvements\n\n");
80 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
81 for change in finding_changes
82 .iter()
83 .filter(|change| change.change == "removed")
84 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
85 {
86 append_finding_highlight_row(out, change);
87 }
88 append_omitted_summary_note(out, removed_count, "removed finding change");
89 out.push('\n');
90 }
91}
92
93fn append_finding_highlight_row(out: &mut String, change: &DiffFindingChange<'_>) {
94 out.push_str(&format!(
95 "| `{}` | `{}` | `{}` | `{}` |\n",
96 markdown_cell(change.change),
97 markdown_cell(change.kind),
98 markdown_cell(change.family.unwrap_or("")),
99 markdown_cell(change.path)
100 ));
101}
102
103fn append_policy_highlights(out: &mut String, policy_changes: &[DiffPolicyChange<'_>]) {
104 let attention_count = policy_changes
105 .iter()
106 .filter(|change| change.severity != "improvement")
107 .count();
108 if attention_count > 0 {
109 out.push_str("### Policy Attention\n\n");
110 out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
111 for change in policy_changes
112 .iter()
113 .filter(|change| change.severity != "improvement")
114 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
115 {
116 append_policy_highlight_row(out, change);
117 }
118 append_omitted_summary_note(out, attention_count, "policy attention change");
119 out.push('\n');
120 }
121
122 let improvement_count = policy_changes
123 .iter()
124 .filter(|change| change.severity == "improvement")
125 .count();
126 if improvement_count > 0 {
127 out.push_str("### Policy Improvements\n\n");
128 out.push_str("| Allow ID | Kind | Detail | Message |\n|---|---|---|---|\n");
129 for change in policy_changes
130 .iter()
131 .filter(|change| change.severity == "improvement")
132 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
133 {
134 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
135 out.push_str(&format!(
136 "| `{}` | `{}` | {} | {} |\n",
137 markdown_cell(change.allow_id),
138 markdown_cell(change.kind),
139 markdown_cell(&detail),
140 markdown_cell(change.message)
141 ));
142 }
143 append_omitted_summary_note(out, improvement_count, "policy improvement change");
144 out.push('\n');
145 }
146}
147
148fn append_omitted_summary_note(out: &mut String, count: usize, singular_label: &str) {
149 if count > PR_SUMMARY_HIGHLIGHT_LIMIT {
150 let omitted = count - PR_SUMMARY_HIGHLIGHT_LIMIT;
151 let plural = if omitted == 1 { "" } else { "s" };
152 out.push_str(&format!(
153 "\n{omitted} additional {singular_label}{plural} omitted from this summary.\n"
154 ));
155 }
156}
157
158fn append_policy_highlight_row(out: &mut String, change: &DiffPolicyChange<'_>) {
159 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
160 out.push_str(&format!(
161 "| `{}` | `{}` | `{}` | {} | {} |\n",
162 markdown_cell(change.severity),
163 markdown_cell(change.allow_id),
164 markdown_cell(change.kind),
165 markdown_cell(&detail),
166 markdown_cell(change.message)
167 ));
168}
169
170pub fn insert_markdown_pr_summary(text: &mut String, summary: &str) {
171 let marker = "Findings scanned:";
172 if let Some(index) = text.find(marker) {
173 text.insert_str(index, summary);
174 } else {
175 text.push('\n');
176 text.push_str(summary);
177 }
178}
179
180pub fn render_diff_finding_changes_markdown(changes: &[DiffFindingChange<'_>]) -> String {
181 let mut out = String::new();
182 out.push_str("\n## Finding Posture Changes\n\n");
183 if changes.is_empty() {
184 out.push_str("No source finding posture changes detected.\n");
185 return out;
186 }
187 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
188 for change in changes.iter().take(120) {
189 out.push_str(&format!(
190 "| `{}` | `{}` | `{}` | `{}` |\n",
191 markdown_cell(change.change),
192 markdown_cell(change.kind),
193 markdown_cell(change.family.unwrap_or("")),
194 markdown_cell(change.path)
195 ));
196 }
197 if changes.len() > 120 {
198 out.push_str(&format!(
199 "\n{} additional finding posture changes omitted.\n",
200 changes.len() - 120
201 ));
202 }
203 out
204}
205
206pub fn render_diff_policy_changes_markdown(changes: &[DiffPolicyChange<'_>]) -> String {
207 let mut out = String::new();
208 out.push_str("\n## Policy Posture Changes\n\n");
209 if changes.is_empty() {
210 out.push_str("No policy weakening detected.\n");
211 return out;
212 }
213 out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
214 for change in changes {
215 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
216 out.push_str(&format!(
217 "| `{}` | `{}` | `{}` | {} | {} |\n",
218 markdown_cell(change.severity),
219 markdown_cell(change.allow_id),
220 markdown_cell(change.kind),
221 markdown_cell(&detail),
222 markdown_cell(change.message)
223 ));
224 }
225 out
226}