cargo-allow 0.1.11

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{CargoAllowError, CargoAllowResult, effective_lane_posture_for_findings};
use allow_match::{CheckMode, evaluate};
use allow_report::{
    RECEIPT_ENFORCEMENT_ADVISORY, RECEIPT_ENFORCEMENT_ENFORCING, ReportContext, Summary,
    render_error_receipt, render_receipt_with_context_and_inventory,
};
use std::env;
use std::path::{Path, PathBuf};
use std::process;

#[path = "check_args.rs"]
mod check_args;
pub(crate) use check_args::CheckArgs;
#[path = "check_lane_posture.rs"]
mod check_lane_posture;
use check_lane_posture::check_failed_for_outcomes;
#[path = "check_deny.rs"]
mod check_deny;
use check_deny::{deny_escalation_failed, validate_deny_statuses};

use crate::federation_report::FederationReportBundle;
use crate::{
    EvidenceReportSummary, EvidenceValidationMode, InventoryFacts, ProfileArg, ReportRenderArgs,
    SourceTreeReportContext, assert_path_within_root, config_path,
    evidence_inventory::current_evidence_source_tree_files, load_compat_world,
    load_world_with_evidence_mode, policy_baseline_debt_entries, print_report, report_config,
    spec_system, write_file,
};
use allow_inventory::{InventorySource, resolve_source_tree_root};

pub(crate) fn cmd_check(args: &CheckArgs) -> CargoAllowResult<()> {
    if matches!(args.profile, Some(ProfileArg::SpecSystem)) {
        reject_source_exception_options(
            args.compat,
            args.kind.as_deref(),
            args.include_untracked,
            &args.deny,
        )?;
        return spec_system::cmd_spec_system(spec_system::SpecSystemCommandArgs {
            command: "check",
            root: &args.root,
            config: args.config.as_deref(),
            format: args.format,
            output: args.output.as_deref(),
            receipt: args.receipt.as_deref(),
        });
    }

    match cmd_check_source_tree(args) {
        Ok(()) => Ok(()),
        Err(err) => {
            if let Some(path) = &args.receipt {
                write_check_error_receipt(path, args, &err)?;
            }
            Err(err)
        }
    }
}

fn cmd_check_source_tree(args: &CheckArgs) -> CargoAllowResult<()> {
    // Validate --output and --receipt paths are within the resolved source-tree
    // root (#1791). The root must be resolved from --root + cwd, not from the
    // process cwd alone: callers regularly run with an out-of-tree --root and a
    // --receipt/--output nested under that root.
    let resolved_root = resolve_check_root(args)?;
    if let Some(output) = &args.output {
        assert_path_within_root(&resolved_root, output)?;
    }
    if let Some(receipt) = &args.receipt {
        assert_path_within_root(&resolved_root, receipt)?;
    }
    let (root, cfg, findings, inventory_facts, federation) = if args.compat {
        let (root, cfg, findings, inventory_facts) = load_compat_world(
            args.root.root.as_deref(),
            args.config.as_deref(),
            args.kind.as_deref(),
            args.include_untracked,
        )?;
        (
            root,
            cfg,
            findings,
            inventory_facts,
            crate::world::default_federation_evaluation(),
        )
    } else {
        load_world_with_evidence_mode(
            args.root.root.as_deref(),
            args.config.as_deref(),
            true,
            args.kind.as_deref(),
            args.include_untracked,
            EvidenceValidationMode::ReportOnly,
        )?
    };
    let federation_bundle = FederationReportBundle::from_evaluation(&federation);
    let report_cfg = report_config(&cfg, args.kind.as_deref())?;
    let mode = CheckMode::parse(
        args.mode
            .as_deref()
            .unwrap_or(report_cfg.workspace.default_mode.as_str()),
    );
    let outcomes = evaluate(&report_cfg, &findings, mode);
    let projected_outcomes = allow_report::ledger_project_outcomes(
        &report_cfg,
        &outcomes,
        allow_core::SimpleDate::today_utc_approx(),
    );
    let evidence_source_tree_files =
        current_evidence_source_tree_files(&root, args.include_untracked);
    let evidence = EvidenceReportSummary::from_policy_with_source_tree_files(
        &root,
        &report_cfg,
        &outcomes,
        evidence_source_tree_files.as_ref(),
    );
    let summary = Summary::from_outcomes(&projected_outcomes);
    let baseline_debt_entries = policy_baseline_debt_entries(&report_cfg);
    let source_context = SourceTreeReportContext::new(&root, inventory_facts);
    let mut context = source_context.report(Some(baseline_debt_entries));
    evidence.apply_to(&mut context);
    let mirror_divergence_count = federation_bundle.mirror_divergence_advisory_count();
    if mirror_divergence_count > 0 {
        context.mirror_divergence_entries = Some(mirror_divergence_count);
    }
    if !args.deny.is_empty() {
        validate_deny_statuses(&args.deny, &summary, context)?;
    }
    let failed = check_failed_for_outcomes(&outcomes, &findings, &report_cfg, mode)
        || evidence.has_broken_evidence_links()
        || federation_bundle.has_blocking_divergence()
        || (!args.deny.is_empty() && deny_escalation_failed(&args.deny, &summary, context));
    if should_emit_report_stdout(args.output.as_deref(), args.receipt.as_deref(), args.format) {
        print_report(ReportRenderArgs {
            command: "check",
            format: args.format,
            baseline_debt_entries,
            evidence,
            findings: &findings,
            outcomes: &projected_outcomes,
            failed,
            output: args.output.as_deref(),
            root: &root,
            inventory_facts,
        })?;
    }
    if let Some(path) = &args.receipt {
        let policy_config =
            config_path(&root, args.config.as_deref()).map(|path| path.display().to_string());
        let effective_mode = args
            .mode
            .as_deref()
            .unwrap_or(report_cfg.workspace.default_mode.as_str());
        let lane_posture = effective_lane_posture_for_findings(
            &report_cfg.lanes,
            findings.iter().map(|finding| finding.kind),
        );
        let provenance = run_provenance();
        let receipt = federation_bundle.with_context(|federation_context| {
            let mut receipt_context = source_context.report(Some(baseline_debt_entries));
            evidence.apply_to(&mut receipt_context);
            if mirror_divergence_count > 0 {
                receipt_context.mirror_divergence_entries = Some(mirror_divergence_count);
            }
            apply_receipt_run_metadata(
                &mut receipt_context,
                effective_mode,
                mode,
                policy_config.as_deref(),
                &provenance.started_at,
                &provenance.run_id,
            );
            receipt_context.lane_posture = Some(&lane_posture);
            receipt_context.federation = Some(federation_context);
            render_receipt_with_context_and_inventory(
                "check",
                &findings,
                &projected_outcomes,
                failed,
                receipt_context,
            )
        });
        write_file(path, &receipt)?;
    }
    if failed {
        process::exit(1);
    }
    Ok(())
}

fn write_check_error_receipt(
    path: &Path,
    args: &CheckArgs,
    err: &CargoAllowError,
) -> CargoAllowResult<()> {
    let root = resolve_check_root(args)?;
    // Never write an error receipt outside the source-tree root: if the run
    // failed because --receipt escaped the root, writing the error receipt
    // there would itself violate the containment contract (#1791).
    assert_path_within_root(&root, path)?;
    let mode = args
        .mode
        .as_deref()
        .map(CheckMode::parse)
        .unwrap_or(CheckMode::NoNew);
    let default_cfg = allow_core::AllowConfig::empty();
    let policy_config =
        config_path(&root, args.config.as_deref()).map(|path| path.display().to_string());
    let effective_mode = args
        .mode
        .as_deref()
        .unwrap_or(default_cfg.workspace.default_mode.as_str());
    let source_context = SourceTreeReportContext::new(
        &root,
        InventoryFacts::source_only(InventorySource::FilesystemFallback),
    );
    let mut context = source_context.report(None);
    let provenance = run_provenance();
    apply_receipt_run_metadata(
        &mut context,
        effective_mode,
        mode,
        policy_config.as_deref(),
        &provenance.started_at,
        &provenance.run_id,
    );
    write_file(path, &render_error_receipt(&err.to_string(), context))
}

fn should_emit_report_stdout(
    output: Option<&Path>,
    receipt: Option<&Path>,
    format: crate::OutputFormat,
) -> bool {
    if output.is_some() {
        return true;
    }
    !(receipt.is_some() && format == crate::OutputFormat::Human)
}

fn apply_receipt_run_metadata<'a>(
    context: &mut ReportContext<'a>,
    effective_mode: &'a str,
    mode: CheckMode,
    policy_config: Option<&'a str>,
    started_at: &'a str,
    run_id: &'a str,
) {
    context.mode = Some(effective_mode);
    context.enforcement = Some(if mode.is_advisory() {
        RECEIPT_ENFORCEMENT_ADVISORY
    } else {
        RECEIPT_ENFORCEMENT_ENFORCING
    });
    context.policy_config = policy_config;
    context.tool_version = Some(env!("CARGO_PKG_VERSION"));
    // Run provenance (#1854): started_at + run_id so a consumer can correlate a
    // receipt to a specific CI run / wall-clock time. Receipts with timestamps
    // are NOT byte-stable across runs (documented).
    context.started_at = Some(started_at);
    context.run_id = Some(run_id);
}

/// Run provenance: a UTC timestamp + a process-unique run id (#1854).
struct RunProvenance {
    started_at: String,
    run_id: String,
}

fn run_provenance() -> RunProvenance {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    // RFC 3339 UTC timestamp from epoch seconds (no chrono dependency).
    let days = (secs / 86_400) as i64;
    let time_of_day = secs % 86_400;
    let date = allow_core::SimpleDate::from_days_since_unix_epoch(days);
    let h = time_of_day / 3600;
    let m = (time_of_day % 3600) / 60;
    let s = time_of_day % 60;
    let started_at = format!(
        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
        date.year, date.month, date.day, h, m, s
    );
    let run_id = format!("cargo-allow-{}-{}", std::process::id(), secs);
    RunProvenance { started_at, run_id }
}

fn resolve_check_root(args: &CheckArgs) -> CargoAllowResult<PathBuf> {
    let cwd =
        env::current_dir().map_err(|e| CargoAllowError::new(format!("failed to read cwd: {e}")))?;
    resolve_source_tree_root(args.root.root.as_deref(), cwd)
}

fn reject_source_exception_options(
    compat: bool,
    kind: Option<&str>,
    include_untracked: bool,
    deny: &[String],
) -> CargoAllowResult<()> {
    if compat {
        return Err(CargoAllowError::new(
            "--compat is not supported with --profile spec-system",
        ));
    }
    if kind.is_some() {
        return Err(CargoAllowError::new(
            "--kind is not supported with --profile spec-system",
        ));
    }
    if include_untracked {
        return Err(CargoAllowError::new(
            "--include-untracked is not supported with --profile spec-system",
        ));
    }
    if !deny.is_empty() {
        return Err(CargoAllowError::new(
            "--deny is not supported with --profile spec-system",
        ));
    }
    Ok(())
}

#[cfg(test)]
#[path = "check_receipt_tests.rs"]
mod receipt_tests;