allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use allow_core::{AllowConfig, CargoAllowResult};

use crate::converter_config::config_from_entries;
use crate::converter_dependency_entries::entry_from_dependency_surface_rule;
use crate::converter_executable_entries::entry_from_executable_rule;
use crate::converter_process_network_entries::{entry_from_network_rule, entry_from_process_rule};
use crate::converter_workflow_entries::entries_from_workflow_rule;
use crate::types::{
    LegacyDependencySurfaceRule, LegacyExecutableRule, LegacyNetworkRule, LegacyProcessRule,
    LegacyWorkflowRule,
};

pub(crate) fn config_from_executable_rules(
    table: &toml::Table,
    rules: &[LegacyExecutableRule],
) -> CargoAllowResult<AllowConfig> {
    config_from_entries(table, rules.iter().map(entry_from_executable_rule))
}

pub(crate) fn config_from_workflow_rules(
    table: &toml::Table,
    rules: &[LegacyWorkflowRule],
) -> CargoAllowResult<AllowConfig> {
    config_from_entries(table, rules.iter().flat_map(entries_from_workflow_rule))
}

pub(crate) fn config_from_dependency_surface_rules(
    table: &toml::Table,
    rules: &[LegacyDependencySurfaceRule],
) -> CargoAllowResult<AllowConfig> {
    config_from_entries(table, rules.iter().map(entry_from_dependency_surface_rule))
}

pub(crate) fn config_from_process_rules(
    table: &toml::Table,
    rules: &[LegacyProcessRule],
) -> CargoAllowResult<AllowConfig> {
    config_from_entries(table, rules.iter().map(entry_from_process_rule))
}

pub(crate) fn config_from_network_rules(
    table: &toml::Table,
    rules: &[LegacyNetworkRule],
) -> CargoAllowResult<AllowConfig> {
    config_from_entries(table, rules.iter().map(entry_from_network_rule))
}

#[cfg(test)]
mod tests {
    use super::*;
    use allow_core::FindingKind;
    use std::path::{Path, PathBuf};

    fn parse_table(input: &str) -> toml::Table {
        toml::from_str::<toml::Table>(input)
            .unwrap_or_else(|err| std::panic::panic_any(format!("test TOML parses: {err}")))
    }

    fn policy_table() -> toml::Table {
        parse_table(
            r#"
owner = "policy"
status = "active"
"#,
        )
    }

    #[test]
    fn config_from_executable_rules_preserves_config_and_converted_entry() {
        let table = policy_table();
        let rules = vec![LegacyExecutableRule {
            id: "executable-script".to_string(),
            path: "scripts\\release.ps1".to_string(),
            owner: "release".to_string(),
            reason: "release script is intentionally executable".to_string(),
            interpreter: Some("powershell".to_string()),
            evidence: vec!["docs/release/automation.md".to_string()],
            created: Some("2026-05-11".to_string()),
            review_after: Some("2026-07-01".to_string()),
            expires: Some("2026-08-01".to_string()),
        }];

        let cfg = config_from_executable_rules(&table, &rules)
            .unwrap_or_else(|err| std::panic::panic_any(format!("config converts: {err}")));

        assert_config_metadata(&cfg, 1);
        let entry = single_entry(&cfg);
        assert_eq!(entry.id, "executable-script");
        assert_eq!(entry.kind, FindingKind::PolicyException);
        assert_eq!(entry.family.as_deref(), Some("executable_file"));
        assert_eq!(
            entry.path.as_deref(),
            Some(Path::new("scripts/release.ps1"))
        );
        assert_eq!(entry.owner, "release");
        assert_eq!(entry.classification, "executable_file");
        assert_eq!(
            entry.selector.ast_kind.as_deref(),
            Some("git_executable_file")
        );
        assert_eq!(
            entry.selector.target_fingerprint.as_deref(),
            Some("git-mode:100755")
        );
        assert_eq!(
            entry.evidence,
            vec![
                "docs/release/automation.md".to_string(),
                "legacy-policy:executable-script".to_string(),
                "interpreter:powershell".to_string(),
            ]
        );
    }

    #[test]
    fn config_from_workflow_rules_preserves_file_and_action_entries() {
        let table = policy_table();
        let rules = vec![LegacyWorkflowRule {
            path: ".github\\workflows\\ci.yml".to_string(),
            owner: "platform".to_string(),
            reason: "required CI lane".to_string(),
            permissions: vec!["contents:read".to_string()],
            secrets_used: vec!["CARGO_REGISTRY_TOKEN".to_string()],
            external_actions: vec![
                "actions/checkout@v4".to_string(),
                "dtolnay/rust-toolchain@stable".to_string(),
            ],
            duplicate_of_lane: Some("ci-shadow".to_string()),
            evidence: vec!["docs/ci.md".to_string()],
            created: Some("2026-01-02".to_string()),
            review_after: Some("2026-07-02".to_string()),
            expires: Some("never".to_string()),
        }];

        let cfg = config_from_workflow_rules(&table, &rules)
            .unwrap_or_else(|err| std::panic::panic_any(format!("config converts: {err}")));

        assert_config_metadata(&cfg, 3);
        let file_entry = cfg
            .allow
            .iter()
            .find(|entry| entry.id == "workflow-file-github-workflows-ci-yml")
            .unwrap_or_else(|| std::panic::panic_any("workflow file entry exists"));
        assert_eq!(file_entry.family.as_deref(), Some("github_workflow"));
        assert_eq!(
            file_entry.path.as_deref(),
            Some(Path::new(".github/workflows/ci.yml"))
        );
        assert_eq!(
            file_entry.evidence,
            vec![
                "docs/ci.md".to_string(),
                "legacy-policy:workflow:.github/workflows/ci.yml".to_string(),
                "permission:contents:read".to_string(),
                "secret:CARGO_REGISTRY_TOKEN".to_string(),
                "duplicate_of_lane:ci-shadow".to_string(),
            ]
        );

        let checkout_action = cfg
            .allow
            .iter()
            .find(|entry| {
                entry.id == "workflow-action-github-workflows-ci-yml--actions-checkout-v4"
            })
            .unwrap_or_else(|| std::panic::panic_any("checkout action entry exists"));
        assert_eq!(
            checkout_action.family.as_deref(),
            Some("workflow_external_action")
        );
        assert_eq!(
            checkout_action.selector.ast_kind.as_deref(),
            Some("github_action_uses")
        );
        assert_eq!(
            checkout_action.selector.target_fingerprint.as_deref(),
            Some("action:actions/checkout@v4")
        );
    }

    #[test]
    fn config_from_dependency_surface_rules_preserves_config_and_converted_entry() {
        let table = policy_table();
        let rules = vec![LegacyDependencySurfaceRule {
            id: "dependency-workspace".to_string(),
            pattern: "crates\\core\\Cargo.toml".to_string(),
            is_glob: false,
            surface: "workspace_manifest".to_string(),
            owner: "build".to_string(),
            reason: "Workspace manifest owns dependency declarations.".to_string(),
            broad_glob_reason: None,
            dep_count_at_baseline: Some(42),
            evidence: vec!["test:dependency_surface".to_string()],
            created: Some("2026-01-01".to_string()),
            review_after: Some("2026-10-01".to_string()),
            expires: Some("2027-01-01".to_string()),
        }];

        let cfg = config_from_dependency_surface_rules(&table, &rules)
            .unwrap_or_else(|err| std::panic::panic_any(format!("config converts: {err}")));

        assert_config_metadata(&cfg, 1);
        let entry = single_entry(&cfg);
        assert_eq!(entry.id, "dependency-workspace");
        assert_eq!(entry.family.as_deref(), Some("dependency_surface"));
        assert_eq!(entry.path, Some(PathBuf::from("crates/core/Cargo.toml")));
        assert_eq!(entry.classification, "workspace_manifest");
        assert_eq!(
            entry.selector.ast_kind.as_deref(),
            Some("dependency_surface")
        );
        assert_eq!(
            entry.evidence,
            vec![
                "test:dependency_surface".to_string(),
                "legacy-policy:dependency-workspace".to_string(),
                "surface:workspace_manifest".to_string(),
                "dep_count_at_baseline:42".to_string(),
            ]
        );
    }

    #[test]
    fn config_from_process_rules_preserves_config_and_converted_entry() {
        let table = policy_table();
        let rules = vec![LegacyProcessRule {
            id: "proc-cargo-install".to_string(),
            binary: "cargo".to_string(),
            argv_shape: vec![
                "install".to_string(),
                "cargo-deny".to_string(),
                "--locked".to_string(),
            ],
            network_reach: true,
            called_by: vec![".github\\workflows\\ci.yml".to_string()],
            owner: "ci".to_string(),
            reason: "CI installs a pinned tool.".to_string(),
            evidence: vec!["doc:docs/ci.md".to_string()],
            created: Some("2026-04-01".to_string()),
            review_after: Some("2026-10-01".to_string()),
            expires: Some("2027-04-01".to_string()),
        }];

        let cfg = config_from_process_rules(&table, &rules)
            .unwrap_or_else(|err| std::panic::panic_any(format!("config converts: {err}")));

        assert_config_metadata(&cfg, 1);
        let entry = single_entry(&cfg);
        assert_eq!(entry.id, "proc-cargo-install");
        assert_eq!(entry.family.as_deref(), Some("process_spawn"));
        assert_eq!(
            entry.path.as_deref(),
            Some(Path::new(".github/workflows/ci.yml"))
        );
        assert_eq!(entry.classification, "network_process");
        assert_eq!(
            entry.selector.symbol.as_deref(),
            Some("cargo install cargo-deny --locked")
        );
        assert_eq!(
            entry.selector.target_fingerprint.as_deref(),
            Some("process:cargo install cargo-deny --locked")
        );
        assert_eq!(
            entry.evidence,
            vec![
                "doc:docs/ci.md".to_string(),
                "legacy-policy:proc-cargo-install".to_string(),
                "binary:cargo".to_string(),
                "argv_shape:install cargo-deny --locked".to_string(),
                "network_reach:true".to_string(),
                "called_by:.github/workflows/ci.yml".to_string(),
            ]
        );
    }

    #[test]
    fn config_from_network_rules_preserves_config_and_converted_entry() {
        let table = policy_table();
        let rules = vec![LegacyNetworkRule {
            id: "net-github-api".to_string(),
            destination: "api.github.com".to_string(),
            auth_required: true,
            auth_secret: Some("GITHUB_TOKEN".to_string()),
            lane: "release".to_string(),
            owner: "release".to_string(),
            reason: "Release lane publishes GitHub releases.".to_string(),
            evidence: vec!["doc:docs/release.md".to_string()],
            created: Some("2026-04-01".to_string()),
            review_after: Some("2026-10-01".to_string()),
            expires: Some("2027-04-01".to_string()),
        }];

        let cfg = config_from_network_rules(&table, &rules)
            .unwrap_or_else(|err| std::panic::panic_any(format!("config converts: {err}")));

        assert_config_metadata(&cfg, 1);
        let entry = single_entry(&cfg);
        assert_eq!(entry.id, "net-github-api");
        assert_eq!(entry.family.as_deref(), Some("network_destination"));
        assert_eq!(entry.classification, "authenticated_network");
        assert_eq!(
            entry.path,
            Some(PathBuf::from("policy/network-allowlist.toml"))
        );
        assert_eq!(
            entry.selector.symbol.as_deref(),
            Some("api.github.com lane release")
        );
        assert_eq!(
            entry.selector.target_fingerprint.as_deref(),
            Some("network:api.github.com:auth:true:lane:release")
        );
        assert_eq!(
            entry.evidence,
            vec![
                "doc:docs/release.md".to_string(),
                "legacy-policy:net-github-api".to_string(),
                "destination:api.github.com".to_string(),
                "lane:release".to_string(),
                "auth_required:true".to_string(),
                "auth_secret:GITHUB_TOKEN".to_string(),
            ]
        );
    }

    fn assert_config_metadata(cfg: &AllowConfig, expected_entries: usize) {
        assert_eq!(cfg.schema_version, "0.1");
        assert_eq!(cfg.policy, "cargo-allow");
        assert_eq!(cfg.owner.as_deref(), Some("policy"));
        assert_eq!(cfg.status.as_deref(), Some("active"));
        assert_eq!(cfg.allow.len(), expected_entries);
    }

    fn single_entry(cfg: &AllowConfig) -> &allow_core::AllowEntry {
        let [entry] = cfg.allow.as_slice() else {
            std::panic::panic_any(format!("expected one allow entry, got {}", cfg.allow.len()));
        };
        entry
    }
}