cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{AllowEntry, Finding, FindingKind, MatchOutcome, MatchStatus};

use super::worklist_item_kind::{
    AMBIGUOUS_SELECTOR, BASELINE_DEBT, EXPIRED_ALLOW, INVALID_SELECTOR, MATCHED, MISSING_EVIDENCE,
    MISSING_REQUIRED_FIELD, NEW_UNRECEIPTED_FINDING, OCCURRENCE_LIMIT_EXCEEDED, REVIEW_DUE,
    STALE_ALLOW, UNSAFE_MISSING_EVIDENCE,
};
use super::worklist_priority::{
    DIFFICULTY_MEDIUM, DIFFICULTY_SMALL, RISK_HIGH, RISK_LOW, RISK_MEDIUM,
};

pub(crate) fn work_item_kind(
    outcome: &MatchOutcome,
    finding: Option<&Finding>,
    entry: Option<&AllowEntry>,
) -> String {
    match outcome.status {
        MatchStatus::New if outcome.allow_id.is_some() => OCCURRENCE_LIMIT_EXCEEDED.to_string(),
        MatchStatus::New => NEW_UNRECEIPTED_FINDING.to_string(),
        MatchStatus::Expired => EXPIRED_ALLOW.to_string(),
        MatchStatus::Stale => STALE_ALLOW.to_string(),
        MatchStatus::Ambiguous => AMBIGUOUS_SELECTOR.to_string(),
        MatchStatus::EvidenceMissing
            if finding
                .map(|finding| finding.kind == FindingKind::Unsafe)
                .or_else(|| entry.map(|entry| entry.kind == FindingKind::Unsafe))
                .unwrap_or(false) =>
        {
            UNSAFE_MISSING_EVIDENCE.to_string()
        }
        MatchStatus::EvidenceMissing => MISSING_EVIDENCE.to_string(),
        MatchStatus::MissingRequiredField => MISSING_REQUIRED_FIELD.to_string(),
        MatchStatus::InvalidSelector => INVALID_SELECTOR.to_string(),
        MatchStatus::BaselineDebt => BASELINE_DEBT.to_string(),
        MatchStatus::ReviewDue => REVIEW_DUE.to_string(),
        MatchStatus::Matched => MATCHED.to_string(),
    }
}

pub(super) fn work_item_risk(
    kind: &str,
    status: MatchStatus,
    finding: Option<&Finding>,
    entry: Option<&AllowEntry>,
) -> &'static str {
    let exception_kind = finding
        .map(|finding| finding.kind)
        .or_else(|| entry.map(|entry| entry.kind));
    let family = exception_family(finding, entry);
    if matches!(status, MatchStatus::Stale) {
        return RISK_LOW;
    }
    if matches!(
        (exception_kind, family),
        (
            Some(FindingKind::PolicyException),
            Some("process_spawn" | "network_destination")
        )
    ) {
        return RISK_HIGH;
    }
    if matches!(exception_kind, Some(FindingKind::Unsafe)) {
        return RISK_HIGH;
    }
    match (kind, status) {
        (AMBIGUOUS_SELECTOR, _) | (_, MatchStatus::Expired) => RISK_HIGH,
        (NEW_UNRECEIPTED_FINDING, _) | (OCCURRENCE_LIMIT_EXCEEDED, _) => RISK_MEDIUM,
        (MISSING_EVIDENCE, _) | (MISSING_REQUIRED_FIELD, _) | (INVALID_SELECTOR, _) => RISK_MEDIUM,
        (BASELINE_DEBT, _) | (REVIEW_DUE, _) => RISK_MEDIUM,
        (STALE_ALLOW, _) => RISK_LOW,
        _ => RISK_MEDIUM,
    }
}

pub(super) fn work_item_difficulty(
    kind: &str,
    finding: Option<&Finding>,
    entry: Option<&AllowEntry>,
) -> &'static str {
    let exception_kind = finding
        .map(|finding| finding.kind)
        .or_else(|| entry.map(|entry| entry.kind));
    match kind {
        STALE_ALLOW => DIFFICULTY_SMALL,
        AMBIGUOUS_SELECTOR | INVALID_SELECTOR => DIFFICULTY_SMALL,
        MISSING_REQUIRED_FIELD | MISSING_EVIDENCE => DIFFICULTY_SMALL,
        REVIEW_DUE | BASELINE_DEBT => DIFFICULTY_MEDIUM,
        UNSAFE_MISSING_EVIDENCE => DIFFICULTY_MEDIUM,
        NEW_UNRECEIPTED_FINDING
            if matches!(
                exception_kind,
                Some(FindingKind::NonRustFile | FindingKind::GeneratedCode)
            ) =>
        {
            DIFFICULTY_SMALL
        }
        NEW_UNRECEIPTED_FINDING | OCCURRENCE_LIMIT_EXCEEDED => DIFFICULTY_MEDIUM,
        _ => DIFFICULTY_MEDIUM,
    }
}

pub(super) fn exception_family<'a>(
    finding: Option<&'a Finding>,
    entry: Option<&'a AllowEntry>,
) -> Option<&'a str> {
    finding
        .and_then(|finding| finding.family.as_deref())
        .or_else(|| entry.and_then(|entry| entry.family.as_deref()))
}