cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use super::WorkItem;
use super::worklist_actions::{proof_commands, suggested_actions, suggested_actions_for_context};
use super::worklist_item_kind::{
    BASELINE_DEBT, BROAD_SCOPE, MISSING_EVIDENCE, UNSAFE_MISSING_EVIDENCE,
};
use super::worklist_priority::DIFFICULTY_SMALL;
use super::worklist_scoring::{exception_family, work_item_difficulty, work_item_risk};
use allow_core::{
    AllowConfig, AllowEntry, Finding, MatchOutcome, MatchStatus, allow_entry_broad_scope,
    normalize_path,
};
use allow_diff::selector_precision_score;

pub(super) fn work_items_from_policy_advisories(
    cfg: &AllowConfig,
    findings: &[Finding],
    outcomes: &[MatchOutcome],
    start_index: usize,
) -> Vec<WorkItem> {
    let mut items = Vec::new();
    for entry in &cfg.allow {
        let Some(outcome) = matched_outcome_for_entry(outcomes, entry) else {
            continue;
        };
        let finding = outcome.finding_index.and_then(|idx| findings.get(idx));
        if entry.classification == "baseline_debt" {
            let item_index = start_index + items.len();
            let kind = BASELINE_DEBT.to_string();
            items.push(WorkItem {
                id: format!("work-baseline-debt-{item_index:04}"),
                exception_kind: Some(entry.kind.as_str().to_string()),
                family: exception_family(finding, Some(entry)).map(ToOwned::to_owned),
                owner: Some(entry.owner.clone()),
                classification: Some(entry.classification.clone()),
                reason: Some(entry.reason.clone()),
                created: entry.lifecycle.created.clone(),
                review_after: entry.lifecycle.review_after.clone(),
                expires: entry.lifecycle.expires.clone(),
                evidence_count: Some(entry.evidence.len()),
                selector_precision: Some(selector_precision_score(entry)),
                risk: work_item_risk(&kind, MatchStatus::BaselineDebt, finding, Some(entry)),
                difficulty: work_item_difficulty(&kind, finding, Some(entry)),
                status: MatchStatus::BaselineDebt,
                allow_id: Some(entry.id.clone()),
                finding_index: outcome.finding_index,
                path: finding
                    .map(|finding| normalize_path(&finding.path))
                    .or_else(|| Some(entry.path_or_glob())),
                evidence_reference: None,
                source_package: source_package_name(finding),
                message: format!(
                    "{} is generated baseline_debt and still needs human review",
                    entry.id
                ),
                suggested_actions: suggested_actions_for_context(&kind, finding, Some(entry)),
                proof_commands: proof_commands(&kind, finding, Some(entry)),
                kind,
            });
            continue;
        }
        if entry.evidence.is_empty() {
            let item_index = start_index + items.len();
            let kind = missing_evidence_kind(entry).to_string();
            items.push(WorkItem {
                id: format!("work-{}-{item_index:04}", kind.replace('_', "-")),
                exception_kind: Some(entry.kind.as_str().to_string()),
                family: exception_family(finding, Some(entry)).map(ToOwned::to_owned),
                owner: Some(entry.owner.clone()),
                classification: Some(entry.classification.clone()),
                reason: Some(entry.reason.clone()),
                created: entry.lifecycle.created.clone(),
                review_after: entry.lifecycle.review_after.clone(),
                expires: entry.lifecycle.expires.clone(),
                evidence_count: Some(0),
                selector_precision: Some(selector_precision_score(entry)),
                risk: work_item_risk(&kind, MatchStatus::EvidenceMissing, finding, Some(entry)),
                difficulty: work_item_difficulty(&kind, finding, Some(entry)),
                status: MatchStatus::EvidenceMissing,
                allow_id: Some(entry.id.clone()),
                finding_index: outcome.finding_index,
                path: finding
                    .map(|finding| normalize_path(&finding.path))
                    .or_else(|| Some(entry.path_or_glob())),
                evidence_reference: None,
                source_package: source_package_name(finding),
                message: format!("{} has no evidence references", entry.id),
                suggested_actions: suggested_actions_for_context(&kind, finding, Some(entry)),
                proof_commands: proof_commands(&kind, finding, Some(entry)),
                kind,
            });
        }
        if let Some(scope) = allow_entry_broad_scope(entry) {
            let item_index = start_index + items.len();
            let kind = BROAD_SCOPE.to_string();
            items.push(WorkItem {
                id: format!("work-broad-scope-{item_index:04}"),
                kind: kind.clone(),
                exception_kind: Some(entry.kind.as_str().to_string()),
                family: entry.family.clone(),
                owner: Some(entry.owner.clone()),
                classification: Some(entry.classification.clone()),
                reason: Some(entry.reason.clone()),
                created: entry.lifecycle.created.clone(),
                review_after: entry.lifecycle.review_after.clone(),
                expires: entry.lifecycle.expires.clone(),
                evidence_count: Some(entry.evidence.len()),
                selector_precision: Some(selector_precision_score(entry)),
                risk: work_item_risk(&kind, MatchStatus::Matched, finding, Some(entry)),
                difficulty: DIFFICULTY_SMALL,
                status: MatchStatus::Matched,
                allow_id: Some(entry.id.clone()),
                finding_index: outcome.finding_index,
                path: Some(scope.clone()),
                evidence_reference: None,
                source_package: source_package_name(finding),
                message: format!("{} uses a broad source-tree scope `{}`", entry.id, scope),
                suggested_actions: suggested_actions(BROAD_SCOPE),
                proof_commands: proof_commands(BROAD_SCOPE, finding, Some(entry)),
            });
        }
    }
    items
}

fn matched_outcome_for_entry<'a>(
    outcomes: &'a [MatchOutcome],
    entry: &AllowEntry,
) -> Option<&'a MatchOutcome> {
    outcomes.iter().find(|outcome| {
        outcome.status == MatchStatus::Matched
            && outcome.allow_id.as_deref() == Some(entry.id.as_str())
    })
}

fn missing_evidence_kind(entry: &AllowEntry) -> &'static str {
    if entry.kind == allow_core::FindingKind::Unsafe {
        UNSAFE_MISSING_EVIDENCE
    } else {
        MISSING_EVIDENCE
    }
}

fn source_package_name(finding: Option<&Finding>) -> Option<String> {
    finding.and_then(|finding| finding.source_package_name().map(ToOwned::to_owned))
}