allow-report 0.1.6

Report and receipt rendering for cargo-allow source exception scans.
Documentation
use crate::diff_policy_detail::policy_change_detail;
use crate::diff_posture::{diff_net_posture, diff_posture_summary};
use crate::evidence_repair::evidence_repair_queues_from_counts;
use crate::{DiffFindingChange, DiffPolicyChange};

const DIFF_HUMAN_CHANGE_LIMIT: usize = 120;

pub fn render_diff_posture_summary_human(
    current_failures: usize,
    finding_changes: &[DiffFindingChange<'_>],
    policy_changes: &[DiffPolicyChange<'_>],
) -> String {
    render_diff_posture_summary_human_with_evidence_health(
        current_failures,
        0,
        0,
        finding_changes,
        policy_changes,
    )
}

pub fn render_diff_posture_summary_human_with_evidence_health_counts(
    current_failures: usize,
    broken_evidence_links: usize,
    missing_evidence: usize,
    weak_evidence_references: usize,
    finding_changes: &[DiffFindingChange<'_>],
    policy_changes: &[DiffPolicyChange<'_>],
) -> String {
    render_diff_posture_summary_human_with_evidence_health_counts_inner(
        current_failures,
        broken_evidence_links,
        missing_evidence,
        weak_evidence_references,
        finding_changes,
        policy_changes,
    )
}

pub fn render_diff_posture_summary_human_with_evidence_health(
    current_failures: usize,
    broken_evidence_links: usize,
    weak_evidence_references: usize,
    finding_changes: &[DiffFindingChange<'_>],
    policy_changes: &[DiffPolicyChange<'_>],
) -> String {
    render_diff_posture_summary_human_with_evidence_health_counts_inner(
        current_failures,
        broken_evidence_links,
        0,
        weak_evidence_references,
        finding_changes,
        policy_changes,
    )
}

fn render_diff_posture_summary_human_with_evidence_health_counts_inner(
    current_failures: usize,
    broken_evidence_links: usize,
    missing_evidence: usize,
    weak_evidence_references: usize,
    finding_changes: &[DiffFindingChange<'_>],
    policy_changes: &[DiffPolicyChange<'_>],
) -> String {
    let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
    let posture = diff_net_posture(summary);
    let mut out = String::new();
    out.push_str("\nDiff posture summary:\n");
    out.push_str(&format!("  net_posture: {}\n", posture.as_str()));
    out.push_str(&format!(
        "  reviewer_action: {}\n",
        posture.reviewer_action()
    ));
    out.push_str(&format!(
        "  current_check_failures: {}\n",
        summary.current_failures
    ));
    if broken_evidence_links > 0 {
        out.push_str(&format!(
            "  broken_evidence_links: {broken_evidence_links}\n"
        ));
    }
    if missing_evidence > 0 {
        out.push_str(&format!("  missing_evidence: {missing_evidence}\n"));
    }
    if weak_evidence_references > 0 {
        out.push_str(&format!(
            "  weak_evidence_references: {weak_evidence_references}\n"
        ));
    }
    let evidence_repair_queues = evidence_repair_queues_from_counts(
        broken_evidence_links,
        missing_evidence,
        weak_evidence_references,
    );
    if !evidence_repair_queues.is_empty() {
        out.push_str("  evidence_repair_queues:\n");
        for queue in evidence_repair_queues {
            out.push_str(&format!("    {}\n", queue.command));
        }
    }
    out.push_str(&format!(
        "  new_source_findings: {}\n",
        summary.new_findings
    ));
    out.push_str(&format!(
        "  removed_source_findings: {}\n",
        summary.removed_findings
    ));
    out.push_str(&format!("  policy_failures: {}\n", summary.policy_failures));
    out.push_str(&format!(
        "  policy_review_items: {}\n",
        summary.policy_review_items
    ));
    out.push_str(&format!(
        "  policy_improvements: {}\n",
        summary.policy_improvements
    ));
    out
}

pub fn render_diff_finding_changes_human(changes: &[DiffFindingChange<'_>]) -> String {
    let mut out = String::new();
    out.push_str("\nFinding posture changes:\n");
    if changes.is_empty() {
        out.push_str("  none\n");
        return out;
    }
    append_finding_changes_human_section(&mut out, "Finding attention", changes, "new");
    append_finding_changes_human_section(&mut out, "Finding improvements", changes, "removed");
    let known_changes = ["new", "removed"];
    if changes
        .iter()
        .any(|change| !known_changes.contains(&change.change))
    {
        out.push_str("  Other finding changes:\n");
        let other_count = changes
            .iter()
            .filter(|change| !known_changes.contains(&change.change))
            .count();
        for change in changes
            .iter()
            .filter(|change| !known_changes.contains(&change.change))
            .take(DIFF_HUMAN_CHANGE_LIMIT)
        {
            append_finding_change_human_row(&mut out, change);
        }
        if other_count > DIFF_HUMAN_CHANGE_LIMIT {
            out.push_str(&format!(
                "    ... {} more omitted\n",
                other_count - DIFF_HUMAN_CHANGE_LIMIT
            ));
        }
    }
    out
}

fn append_finding_changes_human_section(
    out: &mut String,
    heading: &str,
    changes: &[DiffFindingChange<'_>],
    change_kind: &str,
) {
    if !changes.iter().any(|change| change.change == change_kind) {
        return;
    }
    out.push_str(&format!("  {heading}:\n"));
    let matching_count = changes
        .iter()
        .filter(|change| change.change == change_kind)
        .count();
    for change in changes
        .iter()
        .filter(|change| change.change == change_kind)
        .take(DIFF_HUMAN_CHANGE_LIMIT)
    {
        append_finding_change_human_row(out, change);
    }
    if matching_count > DIFF_HUMAN_CHANGE_LIMIT {
        out.push_str(&format!(
            "    ... {} more omitted\n",
            matching_count - DIFF_HUMAN_CHANGE_LIMIT
        ));
    }
}

fn append_finding_change_human_row(out: &mut String, change: &DiffFindingChange<'_>) {
    out.push_str(&format!(
        "    {} {}{} at {}\n",
        change.change,
        change.kind,
        change
            .family
            .map(|family| format!(".{family}"))
            .unwrap_or_default(),
        change.path
    ));
}

pub fn render_diff_policy_changes_human(changes: &[DiffPolicyChange<'_>]) -> String {
    let mut out = String::new();
    out.push_str("\nPolicy posture changes:\n");
    if changes.is_empty() {
        out.push_str("  none\n");
        return out;
    }
    append_policy_changes_human_section(&mut out, "Policy failures", changes, "fail");
    append_policy_changes_human_section(&mut out, "Policy review required", changes, "review");
    append_policy_changes_human_section(&mut out, "Policy improvements", changes, "improvement");
    let known_severities = ["fail", "review", "improvement"];
    if changes
        .iter()
        .any(|change| !known_severities.contains(&change.severity))
    {
        out.push_str("  Other policy changes:\n");
        for change in changes
            .iter()
            .filter(|change| !known_severities.contains(&change.severity))
        {
            append_policy_change_human_row(&mut out, change);
        }
    }
    out
}

fn append_policy_changes_human_section(
    out: &mut String,
    heading: &str,
    changes: &[DiffPolicyChange<'_>],
    severity: &str,
) {
    if !changes.iter().any(|change| change.severity == severity) {
        return;
    }
    out.push_str(&format!("  {heading}:\n"));
    for change in changes {
        if change.severity == severity {
            append_policy_change_human_row(out, change);
        }
    }
}

fn append_policy_change_human_row(out: &mut String, change: &DiffPolicyChange<'_>) {
    out.push_str(&format!(
        "    {} {} {}: {}\n",
        change.severity, change.allow_id, change.kind, change.message
    ));
    if let Some(detail) = policy_change_detail(change) {
        out.push_str(&format!("      detail: {detail}\n"));
    }
}