Skip to main content

allow_report/
diff_human.rs

1use crate::diff_policy_detail::policy_change_detail;
2use crate::diff_posture::{diff_net_posture, diff_posture_summary};
3use crate::evidence_repair::evidence_repair_queues_from_counts;
4use crate::{DiffFindingChange, DiffPolicyChange};
5
6const DIFF_HUMAN_CHANGE_LIMIT: usize = 120;
7
8pub fn render_diff_posture_summary_human(
9    current_failures: usize,
10    finding_changes: &[DiffFindingChange<'_>],
11    policy_changes: &[DiffPolicyChange<'_>],
12) -> String {
13    render_diff_posture_summary_human_with_evidence_health(
14        current_failures,
15        0,
16        0,
17        finding_changes,
18        policy_changes,
19    )
20}
21
22pub fn render_diff_posture_summary_human_with_evidence_health_counts(
23    current_failures: usize,
24    broken_evidence_links: usize,
25    missing_evidence: usize,
26    weak_evidence_references: usize,
27    finding_changes: &[DiffFindingChange<'_>],
28    policy_changes: &[DiffPolicyChange<'_>],
29) -> String {
30    render_diff_posture_summary_human_with_evidence_health_counts_inner(
31        current_failures,
32        broken_evidence_links,
33        missing_evidence,
34        weak_evidence_references,
35        finding_changes,
36        policy_changes,
37    )
38}
39
40pub fn render_diff_posture_summary_human_with_evidence_health(
41    current_failures: usize,
42    broken_evidence_links: usize,
43    weak_evidence_references: usize,
44    finding_changes: &[DiffFindingChange<'_>],
45    policy_changes: &[DiffPolicyChange<'_>],
46) -> String {
47    render_diff_posture_summary_human_with_evidence_health_counts_inner(
48        current_failures,
49        broken_evidence_links,
50        0,
51        weak_evidence_references,
52        finding_changes,
53        policy_changes,
54    )
55}
56
57fn render_diff_posture_summary_human_with_evidence_health_counts_inner(
58    current_failures: usize,
59    broken_evidence_links: usize,
60    missing_evidence: usize,
61    weak_evidence_references: usize,
62    finding_changes: &[DiffFindingChange<'_>],
63    policy_changes: &[DiffPolicyChange<'_>],
64) -> String {
65    let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
66    let posture = diff_net_posture(summary);
67    let mut out = String::new();
68    out.push_str("\nDiff posture summary:\n");
69    out.push_str(&format!("  net_posture: {}\n", posture.as_str()));
70    out.push_str(&format!(
71        "  reviewer_action: {}\n",
72        posture.reviewer_action()
73    ));
74    out.push_str(&format!(
75        "  current_check_failures: {}\n",
76        summary.current_failures
77    ));
78    if broken_evidence_links > 0 {
79        out.push_str(&format!(
80            "  broken_evidence_links: {broken_evidence_links}\n"
81        ));
82    }
83    if missing_evidence > 0 {
84        out.push_str(&format!("  missing_evidence: {missing_evidence}\n"));
85    }
86    if weak_evidence_references > 0 {
87        out.push_str(&format!(
88            "  weak_evidence_references: {weak_evidence_references}\n"
89        ));
90    }
91    let evidence_repair_queues = evidence_repair_queues_from_counts(
92        broken_evidence_links,
93        missing_evidence,
94        weak_evidence_references,
95    );
96    if !evidence_repair_queues.is_empty() {
97        out.push_str("  evidence_repair_queues:\n");
98        for queue in evidence_repair_queues {
99            out.push_str(&format!("    {}\n", queue.command));
100        }
101    }
102    out.push_str(&format!(
103        "  new_source_findings: {}\n",
104        summary.new_findings
105    ));
106    out.push_str(&format!(
107        "  removed_source_findings: {}\n",
108        summary.removed_findings
109    ));
110    out.push_str(&format!("  policy_failures: {}\n", summary.policy_failures));
111    out.push_str(&format!(
112        "  policy_review_items: {}\n",
113        summary.policy_review_items
114    ));
115    out.push_str(&format!(
116        "  policy_improvements: {}\n",
117        summary.policy_improvements
118    ));
119    out
120}
121
122pub fn render_diff_finding_changes_human(changes: &[DiffFindingChange<'_>]) -> String {
123    let mut out = String::new();
124    out.push_str("\nFinding posture changes:\n");
125    if changes.is_empty() {
126        out.push_str("  none\n");
127        return out;
128    }
129    append_finding_changes_human_section(&mut out, "Finding attention", changes, "new");
130    append_finding_changes_human_section(&mut out, "Finding improvements", changes, "removed");
131    let known_changes = ["new", "removed"];
132    if changes
133        .iter()
134        .any(|change| !known_changes.contains(&change.change))
135    {
136        out.push_str("  Other finding changes:\n");
137        let other_count = changes
138            .iter()
139            .filter(|change| !known_changes.contains(&change.change))
140            .count();
141        for change in changes
142            .iter()
143            .filter(|change| !known_changes.contains(&change.change))
144            .take(DIFF_HUMAN_CHANGE_LIMIT)
145        {
146            append_finding_change_human_row(&mut out, change);
147        }
148        if other_count > DIFF_HUMAN_CHANGE_LIMIT {
149            out.push_str(&format!(
150                "    ... {} more omitted\n",
151                other_count - DIFF_HUMAN_CHANGE_LIMIT
152            ));
153        }
154    }
155    out
156}
157
158fn append_finding_changes_human_section(
159    out: &mut String,
160    heading: &str,
161    changes: &[DiffFindingChange<'_>],
162    change_kind: &str,
163) {
164    if !changes.iter().any(|change| change.change == change_kind) {
165        return;
166    }
167    out.push_str(&format!("  {heading}:\n"));
168    let matching_count = changes
169        .iter()
170        .filter(|change| change.change == change_kind)
171        .count();
172    for change in changes
173        .iter()
174        .filter(|change| change.change == change_kind)
175        .take(DIFF_HUMAN_CHANGE_LIMIT)
176    {
177        append_finding_change_human_row(out, change);
178    }
179    if matching_count > DIFF_HUMAN_CHANGE_LIMIT {
180        out.push_str(&format!(
181            "    ... {} more omitted\n",
182            matching_count - DIFF_HUMAN_CHANGE_LIMIT
183        ));
184    }
185}
186
187fn append_finding_change_human_row(out: &mut String, change: &DiffFindingChange<'_>) {
188    out.push_str(&format!(
189        "    {} {}{} at {}\n",
190        change.change,
191        change.kind,
192        change
193            .family
194            .map(|family| format!(".{family}"))
195            .unwrap_or_default(),
196        change.path
197    ));
198}
199
200pub fn render_diff_policy_changes_human(changes: &[DiffPolicyChange<'_>]) -> String {
201    let mut out = String::new();
202    out.push_str("\nPolicy posture changes:\n");
203    if changes.is_empty() {
204        out.push_str("  none\n");
205        return out;
206    }
207    append_policy_changes_human_section(&mut out, "Policy failures", changes, "fail");
208    append_policy_changes_human_section(&mut out, "Policy review required", changes, "review");
209    append_policy_changes_human_section(&mut out, "Policy improvements", changes, "improvement");
210    let known_severities = ["fail", "review", "improvement"];
211    if changes
212        .iter()
213        .any(|change| !known_severities.contains(&change.severity))
214    {
215        out.push_str("  Other policy changes:\n");
216        for change in changes
217            .iter()
218            .filter(|change| !known_severities.contains(&change.severity))
219        {
220            append_policy_change_human_row(&mut out, change);
221        }
222    }
223    out
224}
225
226fn append_policy_changes_human_section(
227    out: &mut String,
228    heading: &str,
229    changes: &[DiffPolicyChange<'_>],
230    severity: &str,
231) {
232    if !changes.iter().any(|change| change.severity == severity) {
233        return;
234    }
235    out.push_str(&format!("  {heading}:\n"));
236    for change in changes {
237        if change.severity == severity {
238            append_policy_change_human_row(out, change);
239        }
240    }
241}
242
243fn append_policy_change_human_row(out: &mut String, change: &DiffPolicyChange<'_>) {
244    out.push_str(&format!(
245        "    {} {} {}: {}\n",
246        change.severity, change.allow_id, change.kind, change.message
247    ));
248    if let Some(detail) = policy_change_detail(change) {
249        out.push_str(&format!("      detail: {detail}\n"));
250    }
251}