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    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}