Skip to main content

allow_report/
diff_human.rs

1use crate::diff_finding_detail::structural_identity_summary;
2use crate::diff_policy_detail::policy_change_detail;
3use crate::diff_posture::{
4    diff_evidence_delta_summary, diff_net_posture, diff_posture_summary,
5    diff_structural_delta_summary,
6};
7use crate::evidence_repair::evidence_repair_queues_from_counts;
8use crate::{DiffFindingChange, DiffPolicyChange};
9
10const DIFF_HUMAN_CHANGE_LIMIT: usize = 120;
11
12pub fn render_diff_posture_summary_human(
13    current_failures: usize,
14    finding_changes: &[DiffFindingChange<'_>],
15    policy_changes: &[DiffPolicyChange<'_>],
16) -> String {
17    render_diff_posture_summary_human_with_evidence_health(
18        current_failures,
19        0,
20        0,
21        finding_changes,
22        policy_changes,
23    )
24}
25
26pub fn render_diff_posture_summary_human_with_evidence_health_counts(
27    current_failures: usize,
28    broken_evidence_links: usize,
29    missing_evidence: usize,
30    weak_evidence_references: usize,
31    finding_changes: &[DiffFindingChange<'_>],
32    policy_changes: &[DiffPolicyChange<'_>],
33) -> String {
34    render_diff_posture_summary_human_with_evidence_health_counts_inner(
35        current_failures,
36        broken_evidence_links,
37        missing_evidence,
38        weak_evidence_references,
39        finding_changes,
40        policy_changes,
41    )
42}
43
44pub fn render_diff_posture_summary_human_with_evidence_health(
45    current_failures: usize,
46    broken_evidence_links: usize,
47    weak_evidence_references: usize,
48    finding_changes: &[DiffFindingChange<'_>],
49    policy_changes: &[DiffPolicyChange<'_>],
50) -> String {
51    render_diff_posture_summary_human_with_evidence_health_counts_inner(
52        current_failures,
53        broken_evidence_links,
54        0,
55        weak_evidence_references,
56        finding_changes,
57        policy_changes,
58    )
59}
60
61fn render_diff_posture_summary_human_with_evidence_health_counts_inner(
62    current_failures: usize,
63    broken_evidence_links: usize,
64    missing_evidence: usize,
65    weak_evidence_references: usize,
66    finding_changes: &[DiffFindingChange<'_>],
67    policy_changes: &[DiffPolicyChange<'_>],
68) -> String {
69    let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
70    let posture = diff_net_posture(summary);
71    let mut out = String::new();
72    out.push_str("\nDiff posture summary:\n");
73    out.push_str(&format!("  net_posture: {}\n", posture.as_str()));
74    out.push_str(&format!(
75        "  reviewer_action: {}\n",
76        posture.reviewer_action()
77    ));
78    out.push_str(&format!(
79        "  current_check_failures: {}\n",
80        summary.current_failures
81    ));
82    if broken_evidence_links > 0 {
83        out.push_str(&format!(
84            "  broken_evidence_links: {broken_evidence_links}\n"
85        ));
86    }
87    if missing_evidence > 0 {
88        out.push_str(&format!("  missing_evidence: {missing_evidence}\n"));
89    }
90    if weak_evidence_references > 0 {
91        out.push_str(&format!(
92            "  weak_evidence_references: {weak_evidence_references}\n"
93        ));
94    }
95    let structural_delta = diff_structural_delta_summary(policy_changes);
96    if structural_delta.scope_broadened > 0 {
97        out.push_str(&format!(
98            "  scope_broadened: {}\n",
99            structural_delta.scope_broadened
100        ));
101    }
102    if structural_delta.scope_changed > 0 {
103        out.push_str(&format!(
104            "  scope_changed: {}\n",
105            structural_delta.scope_changed
106        ));
107    }
108    if structural_delta.scope_narrowed > 0 {
109        out.push_str(&format!(
110            "  scope_narrowed: {}\n",
111            structural_delta.scope_narrowed
112        ));
113    }
114    if structural_delta.selector_changed > 0 {
115        out.push_str(&format!(
116            "  selector_changed: {}\n",
117            structural_delta.selector_changed
118        ));
119    }
120    if structural_delta.selector_precision_decreased > 0 {
121        out.push_str(&format!(
122            "  selector_precision_decreased: {}\n",
123            structural_delta.selector_precision_decreased
124        ));
125    }
126    if structural_delta.selector_precision_increased > 0 {
127        out.push_str(&format!(
128            "  selector_precision_increased: {}\n",
129            structural_delta.selector_precision_increased
130        ));
131    }
132    let evidence_delta = diff_evidence_delta_summary(policy_changes);
133    if evidence_delta.evidence_added > 0 {
134        out.push_str(&format!(
135            "  evidence_added: {}\n",
136            evidence_delta.evidence_added
137        ));
138    }
139    if evidence_delta.weak_evidence_added > 0 {
140        out.push_str(&format!(
141            "  weak_evidence_added: {}\n",
142            evidence_delta.weak_evidence_added
143        ));
144    }
145    if evidence_delta.broken_evidence_added > 0 {
146        out.push_str(&format!(
147            "  broken_evidence_added: {}\n",
148            evidence_delta.broken_evidence_added
149        ));
150    }
151    if evidence_delta.evidence_removed > 0 {
152        out.push_str(&format!(
153            "  evidence_removed: {}\n",
154            evidence_delta.evidence_removed
155        ));
156    }
157    if evidence_delta.evidence_removal_failures > 0 {
158        out.push_str(&format!(
159            "  evidence_removal_failures: {}\n",
160            evidence_delta.evidence_removal_failures
161        ));
162    }
163    if evidence_delta.evidence_removal_review_items > 0 {
164        out.push_str(&format!(
165            "  evidence_removal_review_items: {}\n",
166            evidence_delta.evidence_removal_review_items
167        ));
168    }
169    if evidence_delta.evidence_removal_improvements > 0 {
170        out.push_str(&format!(
171            "  evidence_removal_improvements: {}\n",
172            evidence_delta.evidence_removal_improvements
173        ));
174    }
175    if evidence_delta.link_added > 0 {
176        out.push_str(&format!("  link_added: {}\n", evidence_delta.link_added));
177    }
178    if evidence_delta.weak_link_added > 0 {
179        out.push_str(&format!(
180            "  weak_link_added: {}\n",
181            evidence_delta.weak_link_added
182        ));
183    }
184    if evidence_delta.broken_link_added > 0 {
185        out.push_str(&format!(
186            "  broken_link_added: {}\n",
187            evidence_delta.broken_link_added
188        ));
189    }
190    if evidence_delta.link_removed > 0 {
191        out.push_str(&format!(
192            "  link_removed: {}\n",
193            evidence_delta.link_removed
194        ));
195    }
196    if evidence_delta.link_removal_failures > 0 {
197        out.push_str(&format!(
198            "  link_removal_failures: {}\n",
199            evidence_delta.link_removal_failures
200        ));
201    }
202    if evidence_delta.link_removal_review_items > 0 {
203        out.push_str(&format!(
204            "  link_removal_review_items: {}\n",
205            evidence_delta.link_removal_review_items
206        ));
207    }
208    if evidence_delta.link_removal_improvements > 0 {
209        out.push_str(&format!(
210            "  link_removal_improvements: {}\n",
211            evidence_delta.link_removal_improvements
212        ));
213    }
214    let evidence_repair_queues = evidence_repair_queues_from_counts(
215        broken_evidence_links,
216        missing_evidence,
217        weak_evidence_references,
218    );
219    if !evidence_repair_queues.is_empty() {
220        out.push_str("  evidence_repair_queues:\n");
221        for queue in evidence_repair_queues {
222            out.push_str(&format!("    {}\n", queue.command));
223        }
224    }
225    out.push_str(&format!(
226        "  new_source_findings: {}\n",
227        summary.new_findings
228    ));
229    out.push_str(&format!(
230        "  removed_source_findings: {}\n",
231        summary.removed_findings
232    ));
233    out.push_str(&format!("  policy_failures: {}\n", summary.policy_failures));
234    out.push_str(&format!(
235        "  policy_review_items: {}\n",
236        summary.policy_review_items
237    ));
238    out.push_str(&format!(
239        "  policy_improvements: {}\n",
240        summary.policy_improvements
241    ));
242    out
243}
244
245pub fn render_diff_finding_changes_human(changes: &[DiffFindingChange<'_>]) -> String {
246    let mut out = String::new();
247    out.push_str("\nFinding posture changes:\n");
248    if changes.is_empty() {
249        out.push_str("  none\n");
250        return out;
251    }
252    append_finding_changes_human_section(&mut out, "Finding attention", changes, "new");
253    append_finding_changes_human_section(&mut out, "Finding improvements", changes, "removed");
254    let known_changes = ["new", "removed"];
255    if changes
256        .iter()
257        .any(|change| !known_changes.contains(&change.change))
258    {
259        out.push_str("  Other finding changes:\n");
260        let other_count = changes
261            .iter()
262            .filter(|change| !known_changes.contains(&change.change))
263            .count();
264        for change in changes
265            .iter()
266            .filter(|change| !known_changes.contains(&change.change))
267            .take(DIFF_HUMAN_CHANGE_LIMIT)
268        {
269            append_finding_change_human_row(&mut out, change);
270        }
271        if other_count > DIFF_HUMAN_CHANGE_LIMIT {
272            out.push_str(&format!(
273                "    ... {} more omitted\n",
274                other_count - DIFF_HUMAN_CHANGE_LIMIT
275            ));
276        }
277    }
278    out
279}
280
281fn append_finding_changes_human_section(
282    out: &mut String,
283    heading: &str,
284    changes: &[DiffFindingChange<'_>],
285    change_kind: &str,
286) {
287    if !changes.iter().any(|change| change.change == change_kind) {
288        return;
289    }
290    out.push_str(&format!("  {heading}:\n"));
291    let matching_count = changes
292        .iter()
293        .filter(|change| change.change == change_kind)
294        .count();
295    for change in changes
296        .iter()
297        .filter(|change| change.change == change_kind)
298        .take(DIFF_HUMAN_CHANGE_LIMIT)
299    {
300        append_finding_change_human_row(out, change);
301    }
302    if matching_count > DIFF_HUMAN_CHANGE_LIMIT {
303        out.push_str(&format!(
304            "    ... {} more omitted\n",
305            matching_count - DIFF_HUMAN_CHANGE_LIMIT
306        ));
307    }
308}
309
310fn append_finding_change_human_row(out: &mut String, change: &DiffFindingChange<'_>) {
311    let source_package = change
312        .source_package
313        .map(|package| format!(" source_package={package}"))
314        .unwrap_or_default();
315    let identity = change
316        .identity
317        .map(|identity| format!(" identity={}", structural_identity_summary(identity)))
318        .unwrap_or_default();
319    out.push_str(&format!(
320        "    {} {}{} at {}{}{}\n",
321        change.change,
322        change.kind,
323        change
324            .family
325            .map(|family| format!(".{family}"))
326            .unwrap_or_default(),
327        finding_location(change),
328        source_package,
329        identity
330    ));
331}
332
333fn finding_location(change: &DiffFindingChange<'_>) -> String {
334    match (change.line, change.column) {
335        (Some(line), Some(column)) => format!("{}:{line}:{column}", change.path),
336        (Some(line), None) => format!("{}:{line}", change.path),
337        (None, Some(column)) => format!("{} column={column}", change.path),
338        (None, None) => change.path.to_string(),
339    }
340}
341
342pub fn render_diff_policy_changes_human(changes: &[DiffPolicyChange<'_>]) -> String {
343    let mut out = String::new();
344    out.push_str("\nPolicy posture changes:\n");
345    if changes.is_empty() {
346        out.push_str("  none\n");
347        return out;
348    }
349    append_policy_changes_human_section(&mut out, "Policy failures", changes, "fail");
350    append_policy_changes_human_section(&mut out, "Policy review required", changes, "review");
351    append_policy_changes_human_section(&mut out, "Policy improvements", changes, "improvement");
352    let known_severities = ["fail", "review", "improvement"];
353    if changes
354        .iter()
355        .any(|change| !known_severities.contains(&change.severity))
356    {
357        out.push_str("  Other policy changes:\n");
358        for change in changes
359            .iter()
360            .filter(|change| !known_severities.contains(&change.severity))
361        {
362            append_policy_change_human_row(&mut out, change);
363        }
364    }
365    out
366}
367
368fn append_policy_changes_human_section(
369    out: &mut String,
370    heading: &str,
371    changes: &[DiffPolicyChange<'_>],
372    severity: &str,
373) {
374    if !changes.iter().any(|change| change.severity == severity) {
375        return;
376    }
377    out.push_str(&format!("  {heading}:\n"));
378    for change in changes {
379        if change.severity == severity {
380            append_policy_change_human_row(out, change);
381        }
382    }
383}
384
385fn append_policy_change_human_row(out: &mut String, change: &DiffPolicyChange<'_>) {
386    out.push_str(&format!(
387        "    {} {} {}: {}\n",
388        change.severity, change.allow_id, change.kind, change.message
389    ));
390    if let Some(detail) = policy_change_detail(change) {
391        out.push_str(&format!("      detail: {detail}\n"));
392    }
393}