allow-policy-legacy 0.1.9

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

use crate::converter_workflow_support::{lifecycle_from_workflow_rule, slug_id};
use crate::types::LegacyWorkflowRule;

pub(crate) fn workflow_file_entry(rule: &LegacyWorkflowRule) -> AllowEntry {
    let path = normalize_path(&rule.path);
    AllowEntry {
        id: format!("workflow-file-{}", slug_id(&path)),
        kind: FindingKind::PolicyException,
        family: Some("github_workflow".to_string()),
        path: Some(PathBuf::from(&path)),
        glob: None,
        owner: rule.owner.clone(),
        classification: "github_workflow".to_string(),
        reason: rule.reason.clone(),
        evidence: workflow_evidence(rule),
        links: vec![format!("legacy-policy:workflow:{path}")],
        occurrence_limit: None,
        lifecycle: lifecycle_from_workflow_rule(rule),
        selector: Selector {
            ast_kind: Some("github_workflow".to_string()),
            symbol: Some(path.clone()),
            glob: Some(path),
            ..Selector::default()
        },
        last_seen: None,
    }
}

fn workflow_evidence(rule: &LegacyWorkflowRule) -> Vec<String> {
    let mut evidence = rule.evidence.clone();
    evidence.push(format!(
        "legacy-policy:workflow:{}",
        normalize_path(&rule.path)
    ));
    evidence.extend(
        rule.permissions
            .iter()
            .map(|permission| format!("permission:{permission}")),
    );
    evidence.extend(
        rule.secrets_used
            .iter()
            .map(|secret| format!("secret:{secret}")),
    );
    if let Some(lane) = &rule.duplicate_of_lane {
        evidence.push(format!("duplicate_of_lane:{lane}"));
    }
    evidence
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn workflow_file_entry_maps_legacy_rule_to_policy_exception() {
        let rule = workflow_rule();
        let entry = workflow_file_entry(&rule);

        assert_eq!(entry.id, "workflow-file-github-workflows-ci-yml");
        assert_eq!(entry.kind, FindingKind::PolicyException);
        assert_eq!(entry.family.as_deref(), Some("github_workflow"));
        assert_eq!(entry.path, Some(PathBuf::from(".github/workflows/ci.yml")));
        assert_eq!(entry.glob, None);
        assert_eq!(entry.owner, "platform");
        assert_eq!(entry.classification, "github_workflow");
        assert_eq!(entry.reason, "required CI lane");
        assert_eq!(
            entry.evidence,
            vec![
                "docs/ci.md".to_string(),
                "legacy-policy:workflow:.github/workflows/ci.yml".to_string(),
                "permission:contents:read".to_string(),
                "permission:id-token:write".to_string(),
                "secret:CARGO_REGISTRY_TOKEN".to_string(),
                "duplicate_of_lane:ci-shadow".to_string(),
            ]
        );
        assert_eq!(
            entry.links,
            vec!["legacy-policy:workflow:.github/workflows/ci.yml".to_string()]
        );
        assert_eq!(entry.occurrence_limit, None);
        assert_eq!(entry.lifecycle.created.as_deref(), Some("2026-01-02"));
        assert_eq!(entry.lifecycle.review_after.as_deref(), Some("2026-07-02"));
        assert_eq!(entry.lifecycle.expires.as_deref(), Some("never"));
        assert_eq!(entry.selector.ast_kind.as_deref(), Some("github_workflow"));
        assert_eq!(
            entry.selector.symbol.as_deref(),
            Some(".github/workflows/ci.yml")
        );
        assert_eq!(
            entry.selector.glob.as_deref(),
            Some(".github/workflows/ci.yml")
        );
        assert_eq!(entry.selector.target_fingerprint, None);
        assert_eq!(entry.last_seen, None);
    }

    #[test]
    fn workflow_evidence_keeps_minimal_rule_evidence() {
        let rule = LegacyWorkflowRule {
            path: ".github\\workflows\\release.yml".to_string(),
            owner: "release".to_string(),
            reason: "publish lane".to_string(),
            permissions: Vec::new(),
            secrets_used: Vec::new(),
            external_actions: Vec::new(),
            duplicate_of_lane: None,
            evidence: Vec::new(),
            created: None,
            review_after: None,
            expires: None,
        };

        assert_eq!(
            workflow_evidence(&rule),
            vec!["legacy-policy:workflow:.github/workflows/release.yml".to_string()]
        );
    }

    fn workflow_rule() -> LegacyWorkflowRule {
        LegacyWorkflowRule {
            path: ".github\\workflows\\ci.yml".to_string(),
            owner: "platform".to_string(),
            reason: "required CI lane".to_string(),
            permissions: vec!["contents:read".to_string(), "id-token:write".to_string()],
            secrets_used: vec!["CARGO_REGISTRY_TOKEN".to_string()],
            external_actions: vec!["actions/checkout@v4".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()),
        }
    }
}