allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use allow_core::{AllowConfig, AllowEntry, Finding, FindingKind};
use std::path::PathBuf;

pub fn process_findings_from_config(cfg: &AllowConfig) -> Vec<Finding> {
    cfg.allow
        .iter()
        .filter(|entry| {
            entry.kind == FindingKind::PolicyException
                && entry.family.as_deref() == Some("process_spawn")
        })
        .map(process_finding_from_entry)
        .collect()
}

pub fn network_findings_from_config(cfg: &AllowConfig) -> Vec<Finding> {
    cfg.allow
        .iter()
        .filter(|entry| {
            entry.kind == FindingKind::PolicyException
                && entry.family.as_deref() == Some("network_destination")
        })
        .map(network_finding_from_entry)
        .collect()
}

fn process_finding_from_entry(entry: &AllowEntry) -> Finding {
    let path = entry
        .path
        .clone()
        .unwrap_or_else(|| PathBuf::from(entry.path_or_glob()));
    let symbol = entry
        .selector
        .symbol
        .clone()
        .unwrap_or_else(|| entry.id.clone());
    let mut identity = allow_core::StructuralIdentity::new("policy", "process_spawn");
    identity.symbol = Some(symbol.clone());
    identity.target_fingerprint = entry.selector.target_fingerprint.clone();
    Finding {
        kind: FindingKind::PolicyException,
        family: Some("process_spawn".to_string()),
        path,
        span: Some(allow_core::Span { line: 1, column: 1 }),
        identity,
        message: format!("retained process policy entry {symbol}"),
    }
}

fn network_finding_from_entry(entry: &AllowEntry) -> Finding {
    let path = entry
        .path
        .clone()
        .unwrap_or_else(|| PathBuf::from(entry.path_or_glob()));
    let symbol = entry
        .selector
        .symbol
        .clone()
        .unwrap_or_else(|| entry.id.clone());
    let mut identity = allow_core::StructuralIdentity::new("policy", "network_destination");
    identity.symbol = Some(symbol.clone());
    identity.target_fingerprint = entry.selector.target_fingerprint.clone();
    Finding {
        kind: FindingKind::PolicyException,
        family: Some("network_destination".to_string()),
        path,
        span: Some(allow_core::Span { line: 1, column: 1 }),
        identity,
        message: format!("retained network policy entry {symbol}"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use allow_core::{Lifecycle, Requirements, Selector, WorkspaceConfig};

    #[test]
    fn process_findings_from_config_keeps_only_process_policy_entries() {
        let process = process_entry("proc-cargo-install");
        let network = network_entry("net-github-api");
        let unsafe_entry = allow_entry(
            "unsafe-boundary",
            FindingKind::Unsafe,
            Some("unsafe_block"),
            Some("src/ffi.rs"),
            None,
            Selector {
                ast_kind: Some("unsafe_block".to_string()),
                ..Selector::default()
            },
        );
        let cfg = config_with_entries(vec![process, network, unsafe_entry]);

        let findings = process_findings_from_config(&cfg);

        let [finding] = findings.as_slice() else {
            std::panic::panic_any(format!(
                "expected one process finding, got {}",
                findings.len()
            ));
        };
        assert_eq!(finding.kind, FindingKind::PolicyException);
        assert_eq!(finding.family.as_deref(), Some("process_spawn"));
        assert_eq!(finding.path, PathBuf::from(".github/workflows/ci.yml"));
        assert_eq!(finding.span, Some(allow_core::Span { line: 1, column: 1 }));
        assert_eq!(finding.identity.language, "policy");
        assert_eq!(finding.identity.ast_kind, "process_spawn");
        assert_eq!(
            finding.identity.symbol.as_deref(),
            Some("cargo install cargo-deny --locked")
        );
        assert_eq!(
            finding.identity.target_fingerprint.as_deref(),
            Some("process:cargo install cargo-deny --locked")
        );
        assert_eq!(
            finding.message,
            "retained process policy entry cargo install cargo-deny --locked"
        );
    }

    #[test]
    fn network_findings_from_config_keeps_only_network_policy_entries() {
        let process = process_entry("proc-cargo-install");
        let network = network_entry("net-github-api");
        let dependency = allow_entry(
            "dependency-workspace",
            FindingKind::PolicyException,
            Some("dependency_surface"),
            Some("Cargo.toml"),
            None,
            Selector {
                ast_kind: Some("dependency_surface".to_string()),
                ..Selector::default()
            },
        );
        let cfg = config_with_entries(vec![process, network, dependency]);

        let findings = network_findings_from_config(&cfg);

        let [finding] = findings.as_slice() else {
            std::panic::panic_any(format!(
                "expected one network finding, got {}",
                findings.len()
            ));
        };
        assert_eq!(finding.kind, FindingKind::PolicyException);
        assert_eq!(finding.family.as_deref(), Some("network_destination"));
        assert_eq!(finding.path, PathBuf::from("policy/network-allowlist.toml"));
        assert_eq!(finding.span, Some(allow_core::Span { line: 1, column: 1 }));
        assert_eq!(finding.identity.language, "policy");
        assert_eq!(finding.identity.ast_kind, "network_destination");
        assert_eq!(
            finding.identity.symbol.as_deref(),
            Some("api.github.com lane release")
        );
        assert_eq!(
            finding.identity.target_fingerprint.as_deref(),
            Some("network:api.github.com:auth:true:lane:release")
        );
        assert_eq!(
            finding.message,
            "retained network policy entry api.github.com lane release"
        );
    }

    #[test]
    fn process_finding_from_entry_falls_back_to_entry_scope_and_id() {
        let entry = allow_entry(
            "proc-local-tool",
            FindingKind::PolicyException,
            Some("process_spawn"),
            None,
            Some("policy/process-allowlist.toml"),
            Selector::default(),
        );

        let finding = process_finding_from_entry(&entry);

        assert_eq!(finding.path, PathBuf::from("policy/process-allowlist.toml"));
        assert_eq!(finding.identity.symbol.as_deref(), Some("proc-local-tool"));
        assert_eq!(finding.identity.target_fingerprint, None);
        assert_eq!(
            finding.message,
            "retained process policy entry proc-local-tool"
        );
    }

    #[test]
    fn network_finding_from_entry_falls_back_to_entry_scope_and_id() {
        let entry = allow_entry(
            "net-public",
            FindingKind::PolicyException,
            Some("network_destination"),
            None,
            Some("policy/network-allowlist.toml"),
            Selector::default(),
        );

        let finding = network_finding_from_entry(&entry);

        assert_eq!(finding.path, PathBuf::from("policy/network-allowlist.toml"));
        assert_eq!(finding.identity.symbol.as_deref(), Some("net-public"));
        assert_eq!(finding.identity.target_fingerprint, None);
        assert_eq!(finding.message, "retained network policy entry net-public");
    }

    fn config_with_entries(entries: Vec<AllowEntry>) -> AllowConfig {
        AllowConfig {
            schema_version: "0.1".to_string(),
            policy: "cargo-allow".to_string(),
            owner: Some("policy".to_string()),
            status: Some("active".to_string()),
            workspace: WorkspaceConfig::default(),
            requirements: Requirements::default(),
            allow: entries,
        }
    }

    fn process_entry(id: &str) -> AllowEntry {
        allow_entry(
            id,
            FindingKind::PolicyException,
            Some("process_spawn"),
            Some(".github/workflows/ci.yml"),
            None,
            Selector {
                ast_kind: Some("process_spawn".to_string()),
                symbol: Some("cargo install cargo-deny --locked".to_string()),
                target_fingerprint: Some("process:cargo install cargo-deny --locked".to_string()),
                glob: Some(".github/workflows/ci.yml".to_string()),
                ..Selector::default()
            },
        )
    }

    fn network_entry(id: &str) -> AllowEntry {
        allow_entry(
            id,
            FindingKind::PolicyException,
            Some("network_destination"),
            Some("policy/network-allowlist.toml"),
            None,
            Selector {
                ast_kind: Some("network_destination".to_string()),
                symbol: Some("api.github.com lane release".to_string()),
                target_fingerprint: Some(
                    "network:api.github.com:auth:true:lane:release".to_string(),
                ),
                glob: Some("policy/network-allowlist.toml".to_string()),
                ..Selector::default()
            },
        )
    }

    fn allow_entry(
        id: &str,
        kind: FindingKind,
        family: Option<&str>,
        path: Option<&str>,
        glob: Option<&str>,
        selector: Selector,
    ) -> AllowEntry {
        AllowEntry {
            id: id.to_string(),
            kind,
            family: family.map(str::to_string),
            path: path.map(PathBuf::from),
            glob: glob.map(str::to_string),
            owner: "policy".to_string(),
            classification: "policy".to_string(),
            reason: "test fixture".to_string(),
            evidence: Vec::new(),
            links: Vec::new(),
            occurrence_limit: None,
            lifecycle: Lifecycle::empty(),
            selector,
            last_seen: None,
        }
    }
}