complior-cli 0.9.0

AI Act Compliance Scanner & Fixer — CLI
//! Human-readable check label mappings for scan output.

use crate::types::humanize_kebab;

/// Human-readable label for a `check_id`.
pub fn check_label(check_id: &str) -> String {
    let label = match check_id {
        // ── L1: Document & component presence ───────────────
        "ai-disclosure"            => "AI Disclosure Notice",
        "content-marking"          => "Content Marking / Provenance",
        "interaction-logging"      => "Interaction Logging",
        "ai-literacy"              => "AI Literacy Training Policy",
        "ai-literacy-stale"        => "AI Literacy Training Overdue",
        "ai-literacy-incomplete"   => "AI Literacy Training Incomplete",
        "gpai-transparency"        => "GPAI Transparency Docs",
        "compliance-metadata"      => "Compliance Metadata",
        "documentation"            => "Compliance Documentation",
        "passport-presence"        => "Agent Passport",
        "passport-completeness"    => "Passport Completeness",
        "undeclared-permission"    => "Undeclared Code Permission",
        "unused-declared-permission" => "Unused Passport Permission",
        "behavioral-constraints"   => "Behavioral Constraints",
        "industry-detection"       => "Industry Risk Detection",
        "fria"                     => "Fundamental Rights Assessment",
        "art5-screening"           => "Prohibited Practices Screening",
        "technical-documentation"  => "Technical Documentation",
        "incident-report"          => "Incident Report Template",
        "declaration-of-conformity"=> "Declaration of Conformity",
        "monitoring-policy"        => "Post-Market Monitoring Policy",
        "worker-notification"      => "Worker AI Notification",
        "risk-management"          => "Risk Management System",
        "data-governance"          => "Data Governance Policy",
        "qms"                      => "Quality Management System",
        "instructions-for-use"     => "Instructions for Use",
        "gpai-systemic-risk"       => "GPAI Systemic Risk Assessment",

        // ── L2: Document structure validation ───────────────
        "l2-fria"                  => "FRIA Structure Validation",
        "l2-art5-screening"        => "Art. 5 Screening Structure",
        "l2-tech-documentation"    => "Tech Docs Structure",
        "l2-incident-report"       => "Incident Report Structure",
        "l2-declaration-conformity"=> "Declaration Structure",
        "l2-monitoring-policy"     => "Monitoring Policy Structure",
        "l2-worker-notification"   => "Worker Notification Structure",
        "l2-ai-literacy"           => "AI Literacy Doc Structure",
        "l2-risk-management"       => "Risk Management Doc Structure",
        "l2-data-governance"       => "Data Governance Doc Structure",
        "l2-qms"                   => "QMS Doc Structure",
        "l2-instructions-for-use"  => "Instructions for Use Structure",
        "l2-biometrics-ai-policy"  => "Biometrics AI Policy Structure",
        "l2-critical-infra-ai-policy" => "Critical Infra Policy Structure",
        "l2-migration-ai-policy"   => "Migration AI Policy Structure",

        // ── L3: Dependencies & configuration ────────────────
        "l3-dep-scan"              => "Dependency Security Analysis",
        "l3-dep-license"           => "Dependency License Check",
        "l3-dep-vuln"              => "Dependency Vulnerability",
        "l3-ai-sdk-detected"       => "AI SDK Detected",
        "l3-missing-bias-testing"  => "Bias Testing Missing",
        "l3-log-retention"         => "Log Retention Policy",
        "l3-env-config"            => "Environment Configuration",
        "l3-ci-compliance"         => "CI/CD Compliance Check",

        // ── L4: Code patterns & security ────────────────────
        "l4-bare-llm"              => "Bare LLM API Call",
        "l4-security-risk"         => "Security Vulnerability",
        "l4-human-oversight"       => "Human Oversight Mechanism",
        "l4-conformity-assessment" => "Conformity Assessment",
        "l4-disclosure"            => "AI Disclosure in Code",
        "l4-kill-switch"           => "Kill Switch / Feature Flag",
        "l4-logging"               => "AI Interaction Logging",
        "l4-content-marking"       => "Content Watermarking",
        "l4-data-governance"       => "Data Governance Patterns",
        "l4-accuracy-robustness"   => "Accuracy & Robustness Testing",
        "l4-gpai-transparency"     => "GPAI Model Documentation",
        "l4-deployer-monitoring"   => "Deployer Monitoring",
        "l4-record-keeping"        => "Record Keeping / Audit Trail",
        "l4-cybersecurity"         => "Cybersecurity Controls",
        "l4-nhi-clean"             => "Secrets & Credentials Scan",

        // ── Cross-layer verification ────────────────────────
        "cross-doc-code-mismatch"  => "Documentation ↔ Code Mismatch",
        "cross-sdk-no-disclosure"  => "SDK Without AI Disclosure",
        "cross-banned-with-wrapper"=> "Prohibited Pkg + Controls",
        "cross-logging-no-retention"=> "Logging Without Retention",
        "cross-kill-switch-no-test"=> "Kill Switch Without Tests",
        "cross-passport-code-mismatch" => "Passport ↔ Code Mismatch",
        "cross-permission-passport-mismatch" => "Permission ↔ Passport Mismatch",

        // ── Dynamic patterns (prefix match) ─────────────────
        _ => return check_label_dynamic(check_id),
    };
    label.to_string()
}

/// Handle dynamic `check_ids` (l3-banned-*, l4-nhi-*, industry-*).
fn check_label_dynamic(check_id: &str) -> String {
    if let Some(pkg) = check_id.strip_prefix("l3-banned-") {
        return format!("Prohibited Package: {pkg}");
    }
    if let Some(cat) = check_id.strip_prefix("l4-nhi-") {
        return format!("Secrets: {}", humanize_kebab(cat));
    }
    if check_id == "industry-biometrics" {
        return "High-Risk Domain: Biometrics".to_string();
    }
    if check_id == "industry-critical-infra" {
        return "High-Risk Domain: Critical Infrastructure".to_string();
    }
    if check_id == "industry-migration" {
        return "High-Risk Domain: Migration".to_string();
    }
    if check_id == "industry-legal" {
        return "High-Risk Domain: Legal".to_string();
    }
    if let Some(industry) = check_id.strip_prefix("industry-") {
        return format!("High-Risk Domain: {}", humanize_kebab(industry));
    }
    if let Some(doc) = check_id.strip_prefix("l2-") {
        return format!("{} Structure", humanize_kebab(doc));
    }
    // Final fallback
    let (_, name) = crate::types::strip_layer_prefix(check_id);
    humanize_kebab(name)
}

/// Human-readable label for an external `check_id`.
pub fn ext_check_label(check_id: &str) -> String {
    // Match on known rule patterns within the check_id
    if check_id.contains("unsafe-deser-js") {
        return "Unsafe Code Execution (eval/Function)".to_string();
    }
    if check_id.contains("unsafe-deser") {
        return "Unsafe Deserialization (pickle/yaml)".to_string();
    }
    if check_id.contains("bare-call") {
        return "Bare LLM API Call".to_string();
    }
    if check_id.contains("injection") {
        return "Prompt Injection Risk".to_string();
    }
    if check_id.contains("missing-error-handling") {
        return "Missing Error Handling".to_string();
    }
    if check_id.contains("hardcoded-secret") {
        return "Hardcoded Secrets".to_string();
    }
    // Bandit specific rules
    match check_id {
        "ext-bandit-B301" => return "Unsafe Pickle Usage (pickle.loads)".to_string(),
        "ext-bandit-B302" => return "Unsafe Marshal Deserialization".to_string(),
        "ext-bandit-B307" => return "Unsafe eval() Usage".to_string(),
        "ext-bandit-B324" => return "Weak Hash Algorithm (MD5/SHA1)".to_string(),
        "ext-bandit-B403" => return "Pickle Import Warning".to_string(),
        "ext-bandit-B404" => return "Subprocess Import Warning".to_string(),
        "ext-bandit-B501" => return "SSL Verification Disabled".to_string(),
        "ext-bandit-B506" => return "Unsafe YAML Load".to_string(),
        "ext-bandit-B602" => return "Shell Injection (subprocess)".to_string(),
        "ext-bandit-B605" => return "Shell Command (os.system)".to_string(),
        "ext-bandit-B608" => return "SQL Injection Risk".to_string(),
        "ext-bandit-B102" => return "Hardcoded Password (exec_command)".to_string(),
        "ext-bandit-B104" => return "Binding to All Interfaces (0.0.0.0)".to_string(),
        "ext-bandit-B105" => return "Hardcoded Password String".to_string(),
        "ext-bandit-B108" => return "Hardcoded Temp Directory".to_string(),
        "ext-bandit-B113" => return "Request Without Timeout".to_string(),
        _ => {}
    }
    // Generic prefix-based fallback
    if check_id.starts_with("ext-bandit-") {
        let rule = check_id.strip_prefix("ext-bandit-").unwrap_or(check_id);
        return format!("Python Security Issue ({rule})");
    }
    if check_id.starts_with("ext-detect-secrets-") {
        let secret_type = check_id.strip_prefix("ext-detect-secrets-").unwrap_or("");
        return match secret_type {
            "AWS-Access-Key" => "AWS Access Key Exposed".to_string(),
            "Secret-Keyword" => "Secret Keyword Detected".to_string(),
            "Hex-High-Entropy-String" => "High-Entropy Hex String (possible key)".to_string(),
            "Base64-High-Entropy-String" => "High-Entropy Base64 String (possible key)".to_string(),
            "Stripe-Access-Key" => "Stripe API Key Exposed".to_string(),
            "Slack-Webhook" => "Slack Webhook URL Exposed".to_string(),
            "GitHub-Token" => "GitHub Token Exposed".to_string(),
            "Twilio-Access-Token" => "Twilio Token Exposed".to_string(),
            _ if !secret_type.is_empty() => format!("Hardcoded Secret ({})", humanize_kebab(secret_type)),
            _ => "Hardcoded Secret Detected".to_string(),
        };
    }
    if check_id.starts_with("ext-modelscan-") {
        let scanner = check_id.strip_prefix("ext-modelscan-").unwrap_or("");
        return match scanner {
            s if s.contains("PickleUnsafeOp") => "Malicious Pickle Model (code execution)".to_string(),
            s if s.contains("Pickle") => "Unsafe Pickle Model".to_string(),
            s if s.contains("H5") || s.contains("Keras") => "Unsafe HDF5/Keras Model".to_string(),
            _ => "Unsafe Model File".to_string(),
        };
    }
    // Final fallback: strip ext- prefix and humanize
    let stripped = check_id.strip_prefix("ext-semgrep-").or_else(|| check_id.strip_prefix("ext-"))
        .unwrap_or(check_id);
    humanize_kebab(stripped)
}