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