Skip to main content

allow_report/
diff_markdown.rs

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