//! SAST — Static Application Security Testing Engine
//!
//! Research grounding:
//!   - IRIS (ICLR 2025): Neuro-symbolic approach combining pattern matching with
//!     whole-repository taint-flow reasoning. Key insight: single-line pattern matching
//!     produces ~60% false positive rate; taint-flow context reduces it to ~15%.
//!   - MoCQ (arXiv 2025): LLM + classic vulnerability checker pattern generation.
//!   - FDSP (2024): Iterative refinement via static analysis feedback.
//!
//! This engine implements:
//!   1. **55 rules** across 8 CWE categories (language-aware)
//!   2. **Taint-flow simulation**: tracks user-controlled sources across lines
//!      to reduce false positives (inspired by IRIS whole-repo reasoning)
//!   3. **CVSS v3.1-inspired scoring**: impact * exploitability * scope
//!   4. **Fix recommendations**: every rule carries a concrete fix string
//!   5. **False-positive suppression**: test files, comment blocks, constant strings
//!   6. **Confidence scoring** [0.0, 1.0]: accounts for context quality
//!
//! Performance: O(N × R) where N = line count, R = rule count (~55).
//! For typical file sizes (<500 lines) this is ~27,500 operations, microseconds.

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

// ═══════════════════════════════════════════════════════════════════
// Types
// ═══════════════════════════════════════════════════════════════════

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
    Info,
    Low,
    Medium,
    High,
    Critical,
}

impl Severity {
    /// CVSS base score contribution [0.0, 4.0] (used in aggregate scoring)
    pub fn cvss_weight(self) -> f64 {
        match self {
            Severity::Info => 0.5,
            Severity::Low => 1.5,
            Severity::Medium => 3.0,
            Severity::High => 6.5,
            Severity::Critical => 9.5,
        }
    }
}

/// A single SAST rule.
#[derive(Debug, Clone)]
pub struct SastRule {
    pub id: &'static str,
    pub cwe: u32,
    pub severity: Severity,
    pub category: &'static str,
    /// Pattern to look for (case-insensitive substring match)
    pub pattern: &'static str,
    /// Optional: if set, the line must also contain this to fire
    pub requires: Option<&'static str>,
    /// If set, the rule does NOT fire if this is also present (negation)
    pub suppressed_by: Option<&'static str>,
    pub description: &'static str,
    pub fix: &'static str,
    /// Which languages this rule applies to (empty = all)
    pub languages: &'static [&'static str],
    /// Is this rule taint-aware (needs TaintContext to fire)?
    pub taint_aware: bool,
}

/// A single SAST finding.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SastFinding {
    pub rule_id: String,
    pub cwe: u32,
    pub severity: Severity,
    pub category: String,
    pub line_number: usize,
    pub line_content: String,
    /// Confidence [0.0, 1.0] — lower for test files, inline suppressions, etc.
    pub confidence: f64,
    pub description: String,
    pub fix: String,
    /// If this finding is taint-flow sourced (higher confidence than pattern-only)
    pub taint_flow: bool,
}

/// The full result of scanning a fragment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SastReport {
    pub source: String,
    pub findings: Vec<SastFinding>,
    /// CVSS-inspired aggregate risk score [0.0, 10.0]
    pub risk_score: f64,
    /// Breakdown by severity
    pub critical_count: usize,
    pub high_count: usize,
    pub medium_count: usize,
    pub low_count: usize,
    pub info_count: usize,
    /// Top recommended action
    pub top_fix: Option<String>,
}

// ═══════════════════════════════════════════════════════════════════
// Rule Database — 55 rules across 8 categories
// ═══════════════════════════════════════════════════════════════════

static RULES: &[SastRule] = &[
    // ── Category 1: Hardcoded Secrets (CWE-798) ─────────────────────
    SastRule {
        id: "SEC-001", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "password",
        requires: Some("="),
        suppressed_by: Some("env"),
        description: "Hardcoded password detected. Credentials must never be embedded in source code.",
        fix: "Use environment variables or a secrets manager (Vault, AWS Secrets Manager). Reference via os.environ or std::env::var.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-002", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "api_key",
        requires: Some("="),
        suppressed_by: Some("env"),
        description: "Hardcoded API key assignment. API keys embedded in code are frequently leaked via version control.",
        fix: "Load via environment variable: api_key = os.environ['API_KEY']",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-003", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "sk-",
        requires: Some("\""),
        suppressed_by: None,
        description: "Potential OpenAI/Anthropic API key literal (sk-... prefix).",
        fix: "Remove from code. Rotate the key immediately if committed to version control.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-004", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "ghp_",
        requires: Some("\""),
        suppressed_by: None,
        description: "GitHub Personal Access Token literal detected (ghp_ prefix).",
        fix: "Revoke this token immediately at github.com/settings/tokens. Use GITHUB_TOKEN env var.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-005", cwe: 798, severity: Severity::High,
        category: "Hardcoded Secrets",
        pattern: "private_key",
        requires: Some("="),
        suppressed_by: Some("path"),
        description: "Private key assignment in source code.",
        fix: "Load private keys from secure key stores or PEM files outside the repository.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-006", cwe: 798, severity: Severity::High,
        category: "Hardcoded Secrets",
        pattern: "secret",
        requires: Some("="),
        suppressed_by: Some("env"),
        description: "Variable named 'secret' assigned a literal value.",
        fix: "Use a secrets manager or environment variable injection.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-007", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "aws_secret_access_key",
        requires: None,
        suppressed_by: Some("env"),
        description: "AWS Secret Access Key variable found. AWS credentials must never appear in code.",
        fix: "Use IAM roles, AWS Secrets Manager, or environment variables. Run `git-secrets` to prevent future leaks.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SEC-008", cwe: 798, severity: Severity::High,
        category: "Hardcoded Secrets",
        pattern: "jdbc:postgresql://",
        requires: Some("password"),
        suppressed_by: None,
        description: "Database connection string with embedded credentials.",
        fix: "Externalize connection strings to environment variables or a configuration service.",
        languages: &[],
        taint_aware: false,
    },

    // ── Category 2: SQL Injection (CWE-89) ──────────────────────────
    SastRule {
        id: "SQL-001", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "execute(",
        requires: Some("%s"),
        suppressed_by: None,
        description: "String-formatted SQL query via % operator — classic SQL injection vector.",
        fix: "Use parameterized queries: cursor.execute(sql, (param1, param2)). Never format user data into SQL.",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "SQL-002", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "execute(",
        requires: Some(".format("),
        suppressed_by: None,
        description: ".format() call inside SQL execute — SQL injection via string formatting.",
        fix: "Replace with parameterized queries. Use an ORM like SQLAlchemy for type-safe queries.",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "SQL-003", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "raw(",
        requires: Some("request"),
        suppressed_by: None,
        description: "Django raw() query with request data — SQL injection.",
        fix: "Use Django ORM .filter() methods. If raw SQL is necessary, use params=[]: Model.objects.raw(sql, params=[val])",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "SQL-004", cwe: 89, severity: Severity::High,
        category: "SQL Injection",
        pattern: "query!(",
        requires: Some("{"),
        suppressed_by: None,
        description: "Rust sqlx query! macro with inline string interpolation.",
        fix: "Use query! with ? placeholders and bind parameters: sqlx::query!(\"SELECT ... WHERE id = ?\", id)",
        languages: &["rs"],
        taint_aware: true,
    },
    SastRule {
        id: "SQL-005", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "string.concat(",
        requires: Some("select"),
        suppressed_by: None,
        description: "SQL query built via string concatenation.",
        fix: "Use prepared statements with parameter binding appropriate for your database driver.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "SQL-006", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "execute(",
        requires: None,           // No same-line require — rely on taint
        suppressed_by: None,
        description: "execute() called with a tainted (user-derived) variable — SQL injection via dynamic query.",
        fix: "Use parameterized queries: cursor.execute(sql, (param1, param2)). The query string must be static.",
        languages: &["py"],
        taint_aware: true,        // Only fires when a tainted var appears on this line
    },


    // ── Category 3: Path Traversal (CWE-22) ─────────────────────────
    SastRule {
        id: "PATH-001", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "open(",
        requires: Some("request"),
        suppressed_by: Some("safe"),
        description: "File open with request-derived path — potential path traversal.",
        fix: "Canonicalize the path and validate it stays within the allowed base directory: os.path.realpath(). Deny paths containing '..'.",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "PATH-002", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "../",
        requires: None,
        suppressed_by: Some("test"),
        description: "Literal path traversal sequence '../' in code.",
        fix: "Never construct file paths from user input with relative components. Use os.path.abspath() and validate the result.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "PATH-003", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "fs::read",
        requires: Some("input"),
        suppressed_by: None,
        description: "Rust fs::read/read_to_string with input-derived path.",
        fix: "Use Path::new(base).join(user_input) then verify the canonical path starts with base using path.starts_with(base).",
        languages: &["rs"],
        taint_aware: true,
    },
    SastRule {
        id: "PATH-004", cwe: 22, severity: Severity::Medium,
        category: "Path Traversal",
        pattern: "sendfile",
        requires: Some("param"),
        suppressed_by: None,
        description: "sendfile with URL parameter path — path traversal in file serving.",
        fix: "Whitelist acceptable file extensions and validate paths against an allowed directory.",
        languages: &[],
        taint_aware: true,
    },

    // ── Category 4: Command Injection (CWE-78) ───────────────────────
    SastRule {
        id: "CMD-001", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "os.system(",
        requires: None,
        suppressed_by: None,
        description: "os.system() executes shell commands — trivially injectable if arguments contain user input.",
        fix: "Replace with subprocess.run([...], shell=False) with a list of arguments. Never pass user input to shell=True.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "CMD-002", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "shell=true",
        requires: None,
        suppressed_by: None,
        description: "subprocess called with shell=True — enables shell injection.",
        fix: "Use shell=False with a list: subprocess.run(['cmd', arg1, arg2]). Shell metacharacters are then inert.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "CMD-003", cwe: 78, severity: Severity::High,
        category: "Command Injection",
        pattern: "exec(",
        requires: Some("request"),
        suppressed_by: None,
        description: "exec() with request-derived content — arbitrary code execution.",
        fix: "Never exec() user-supplied content. If dynamic dispatch is needed, use a whitelist of safe operations.",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "CMD-004", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "eval(",
        requires: Some("input"),
        suppressed_by: None,
        description: "eval() with user input — arbitrary code execution.",
        fix: "Replace eval() with ast.literal_eval() for safe evaluation of Python literals, or use a proper parser.",
        languages: &["py", "js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "CMD-005", cwe: 78, severity: Severity::High,
        category: "Command Injection",
        pattern: "std::process::command",
        requires: Some("from_utf8"),
        suppressed_by: None,
        description: "Rust Command built from parsed string input — command injection risk.",
        fix: "Validate and sanitize input before passing to Command. Use a fixed command with arguments rather than constructing from strings.",
        languages: &["rs"],
        taint_aware: true,
    },

    // ── Category 5: Insecure Cryptography (CWE-327 / CWE-916) ────────
    SastRule {
        id: "CRYPTO-001", cwe: 327, severity: Severity::High,
        category: "Insecure Cryptography",
        pattern: "md5",
        requires: None,
        suppressed_by: Some("non-cryptographic"),
        description: "MD5 used — broken cryptographic hash. Collisions are trivially constructable.",
        fix: "Replace with SHA-256 (hashlib.sha256) for integrity, or bcrypt/argon2 for password hashing.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-002", cwe: 327, severity: Severity::High,
        category: "Insecure Cryptography",
        pattern: "sha1",
        requires: None,
        suppressed_by: Some("non-cryptographic"),
        description: "SHA-1 used — cryptographically broken since 2017 (SHAttered attack).",
        fix: "Migrate to SHA-256 or SHA-3. For Git compatibility uses, this is acceptable but document it.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-003", cwe: 327, severity: Severity::Critical,
        category: "Insecure Cryptography",
        pattern: "des",
        requires: Some("encrypt"),
        suppressed_by: None,
        description: "DES encryption — 56-bit key is brute-forceable in hours.",
        fix: "Replace with AES-256-GCM. DES was deprecated by NIST in 2005.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-004", cwe: 916, severity: Severity::Critical,
        category: "Insecure Cryptography",
        pattern: "hashlib.md5(",
        requires: Some("password"),
        suppressed_by: None,
        description: "Password hashed with MD5 — reversible in seconds with rainbow tables.",
        fix: "Use bcrypt: bcrypt.hashpw(password, bcrypt.gensalt()) or argon2-cffi for modern password hashing.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-005", cwe: 330, severity: Severity::High,
        category: "Insecure Cryptography",
        pattern: "random.random()",
        requires: None,
        suppressed_by: None,
        description: "Python random.random() is not cryptographically secure — predictable from seed.",
        fix: "Use secrets.token_bytes() or os.urandom() for security-sensitive random values.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-006", cwe: 327, severity: Severity::High,
        category: "Insecure Cryptography",
        pattern: "rc4",
        requires: None,
        suppressed_by: None,
        description: "RC4 stream cipher — banned by RFC 7465, multiple practical attacks exist.",
        fix: "Replace with ChaCha20-Poly1305 or AES-256-GCM for authenticated encryption.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "CRYPTO-007", cwe: 327, severity: Severity::Medium,
        category: "Insecure Cryptography",
        pattern: "ecb",
        requires: Some("mode"),
        suppressed_by: None,
        description: "AES in ECB mode — identical plaintext blocks produce identical ciphertext (penguin attack).",
        fix: "Use AES-GCM or AES-CBC with a random IV. ECB must never be used for data > one block.",
        languages: &[],
        taint_aware: false,
    },

    // ── Category 6: Unsafe Deserialization (CWE-502) ─────────────────
    SastRule {
        id: "DESER-001", cwe: 502, severity: Severity::Critical,
        category: "Unsafe Deserialization",
        pattern: "pickle.loads(",
        requires: None,
        suppressed_by: None,
        description: "pickle.loads() on untrusted data executes arbitrary code during deserialization.",
        fix: "Never unpickle data from untrusted sources. Use JSON, MessagePack, or protobuf for cross-boundary data.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "DESER-002", cwe: 502, severity: Severity::Critical,
        category: "Unsafe Deserialization",
        pattern: "pickle.load(",
        requires: None,
        suppressed_by: None,
        description: "pickle.load() from a file or stream — RCE if file is attacker-controlled.",
        fix: "Validate file provenance (HMAC signature) before unpickling, or switch to json.load().",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "DESER-003", cwe: 502, severity: Severity::High,
        category: "Unsafe Deserialization",
        pattern: "yaml.load(",
        suppressed_by: Some("loader="),
        requires: None,
        description: "yaml.load() without Loader= argument — defaults to unsafe full YAML deserialization.",
        fix: "Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader) to prevent arbitrary object instantiation.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "DESER-004", cwe: 502, severity: Severity::High,
        category: "Unsafe Deserialization",
        pattern: "marshal.loads(",
        requires: None,
        suppressed_by: None,
        description: "marshal.loads() — Python marshal format can execute arbitrary bytecode.",
        fix: "Do not use marshal for untrusted input. JSON or protobuf are safe alternatives.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "DESER-005", cwe: 502, severity: Severity::High,
        category: "Unsafe Deserialization",
        pattern: "json_decode",
        requires: Some("allow_classes"),
        suppressed_by: None,
        description: "PHP unserialize with class instantiation — common PHP RCE vector.",
        fix: "Avoid unserialize() on untrusted input. Use json_decode() with explicit type checking.",
        languages: &["php"],
        taint_aware: false,
    },

    // ── Category 7: XSS / Injection (CWE-79 / CWE-94) ───────────────
    SastRule {
        id: "XSS-001", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "innerhtml",
        requires: Some("="),
        suppressed_by: Some("sanitize"),
        description: "innerHTML assignment — XSS if content is user-controlled.",
        fix: "Use textContent instead of innerHTML, or sanitize with DOMPurify.sanitize() before assignment.",
        languages: &["js", "ts"],
        taint_aware: false,  // innerHTML = x is always risky regardless of taint source
    },
    SastRule {
        id: "XSS-002", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "dangerouslysetinnerhtml",
        requires: None,
        suppressed_by: Some("dompurify"),
        description: "React dangerouslySetInnerHTML — bypasses React's XSS protections.",
        fix: "Sanitize with DOMPurify before passing to dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(content) }",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "XSS-003", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "document.write(",
        requires: None,
        suppressed_by: None,
        description: "document.write() with dynamic content — XSS vector.",
        fix: "Replace with DOM manipulation: document.createElement() + textContent for safe content insertion.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "XSS-004", cwe: 79, severity: Severity::Medium,
        category: "XSS",
        pattern: "mark_safe(",
        requires: None,
        suppressed_by: None,
        description: "Django mark_safe() bypasses auto-escaping — XSS if applied to user data.",
        fix: "Only call mark_safe() on strings you have constructed yourself, never on user input.",
        languages: &["py"],
        taint_aware: true,
    },
    SastRule {
        id: "XSS-005", cwe: 94, severity: Severity::Critical,
        category: "XSS",
        pattern: "eval(",
        requires: None,
        suppressed_by: None,
        description: "JavaScript eval() — XSS / remote code execution if argument is user-controlled.",
        fix: "Eliminate eval(). Use JSON.parse() for data, or Function() constructor with strict input validation.",
        languages: &["js", "ts"],
        taint_aware: true,
    },

    // ── Category 8: Insecure Config / Auth (CWE-285, CWE-306) ────────
    SastRule {
        id: "AUTH-001", cwe: 306, severity: Severity::Critical,
        category: "Insecure Auth",
        pattern: "debug=true",
        requires: None,
        suppressed_by: Some("development"),
        description: "Debug mode enabled — exposes stack traces, internal routes, and disables security middleware.",
        fix: "Set DEBUG = False in production and configure DEBUG via environment variable only.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "AUTH-002", cwe: 285, severity: Severity::Critical,
        category: "Insecure Auth",
        pattern: "allow_all_origins=true",
        requires: None,
        suppressed_by: None,
        description: "CORS configured to allow all origins — bypasses browser same-origin protection.",
        fix: "Set CORS_ALLOWED_ORIGINS to an explicit whitelist of trusted domains.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "AUTH-003", cwe: 306, severity: Severity::High,
        category: "Insecure Auth",
        pattern: "verify=false",
        requires: Some("ssl"),
        suppressed_by: None,
        description: "SSL certificate verification disabled — vulnerable to man-in-the-middle attacks.",
        fix: "Remove verify=False. If using self-signed certs in dev, configure a custom CA bundle instead.",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "AUTH-004", cwe: 285, severity: Severity::High,
        category: "Insecure Auth",
        pattern: "skip_auth",
        requires: Some("="),
        suppressed_by: Some("test"),
        description: "Authentication skip flag — if this reaches production, auth is bypassed.",
        fix: "Remove skip_auth flags. Use feature flags gated by environment, never code-level booleans.",
        languages: &[],
        taint_aware: false,
    },
    SastRule {
        id: "AUTH-005", cwe: 284, severity: Severity::Critical,
        category: "Insecure Auth",
        pattern: "is_admin",
        requires: Some("request.params"),
        suppressed_by: None,
        description: "Admin status derived from request parameters — trivially forgeable.",
        fix: "Admin checks must come from the authenticated session, not request parameters.",
        languages: &[],
        taint_aware: true,
    },
    SastRule {
        id: "AUTH-006", cwe: 306, severity: Severity::High,
        category: "Insecure Auth",
        pattern: "permitall",
        requires: None,
        suppressed_by: Some("test"),
        description: "Spring Security permitAll() — disables authentication for matched routes.",
        fix: "Explicitly list permitted paths. Avoid blanket permitAll() in production security configs.",
        languages: &["java"],
        taint_aware: false,
    },
    SastRule {
        id: "AUTH-007", cwe: 798, severity: Severity::Critical,
        category: "Insecure Auth",
        pattern: "jwt.decode(",
        requires: Some("\"none\""),  // matches algorithms=["none"] (quoted none)
        suppressed_by: None,
        description: "JWT decoded with algorithms=[\"none\"] — allows unsigned tokens, trivially forgeable.",
        fix: "Always specify a concrete algorithm list: jwt.decode(token, key, algorithms=[\"HS256\"])",
        languages: &["py"],
        taint_aware: false,
    },

    // ── Category 9: Memory Safety - Rust-specific (CWE-119) ──────────
    SastRule {
        id: "MEM-001", cwe: 119, severity: Severity::High,
        category: "Memory Safety",
        pattern: "unsafe {",
        requires: None,
        suppressed_by: Some("safety:"),  // matches '// SAFETY:' or '# safety:' on prev/same line
        description: "Unsafe block without a safety comment explaining the invariants maintained.",
        fix: "Add a // SAFETY: comment above the unsafe block explaining why this is safe. Consider if a safe abstraction is possible.",
        languages: &["rs"],
        taint_aware: false,
    },
    SastRule {
        id: "MEM-002", cwe: 119, severity: Severity::Critical,
        category: "Memory Safety",
        pattern: "from_raw_parts(",
        requires: None,
        suppressed_by: Some("// safety:"),
        description: "slice::from_raw_parts without documented safety invariants.",
        fix: "Document length/alignment/lifetime guarantees in a SAFETY comment. Prefer safe slice operations.",
        languages: &["rs"],
        taint_aware: false,
    },
    SastRule {
        id: "MEM-003", cwe: 476, severity: Severity::High,
        category: "Memory Safety",
        pattern: "unwrap()",
        requires: Some("option"),
        suppressed_by: None,
        description: "Option::unwrap() panics on None — production code should handle None explicitly.",
        fix: "Replace with .expect(\"descriptive message\") for debugging, or propagate with ? / if let.",
        languages: &["rs"],
        taint_aware: false,
    },

    // ── Category 10: Logging / Information Disclosure (CWE-532) ───────
    SastRule {
        id: "LOG-001", cwe: 532, severity: Severity::Medium,
        category: "Information Disclosure",
        pattern: "print(",
        requires: Some("password"),
        suppressed_by: None,
        description: "Logging or printing a value named 'password' — credential leakage to logs/stdout.",
        fix: "Never log credentials. Redact sensitive fields: log.info('auth attempted for user %s', user) (no password).",
        languages: &["py"],
        taint_aware: false,
    },
    SastRule {
        id: "LOG-002", cwe: 532, severity: Severity::Medium,
        category: "Information Disclosure",
        pattern: "console.log(",
        requires: Some("token"),
        suppressed_by: None,
        description: "console.log of a token value — tokens appear in browser DevTools and CI logs.",
        fix: "Remove token logging. Use structured logging with explicit field filtering for sensitive values.",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "LOG-003", cwe: 209, severity: Severity::Medium,
        category: "Information Disclosure",
        pattern: "traceback.print_exc",
        requires: Some("response"),
        suppressed_by: None,
        description: "Stack trace returned in HTTP response — leaks implementation details to attackers.",
        fix: "Log stack traces server-side only. Return generic error messages to clients.",
        languages: &["py"],
        taint_aware: false,
    },

    // ── Category 11: Node.js-specific (CWE-78, CWE-22, CWE-89, CWE-1321) ──
    SastRule {
        id: "NODE-001", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "child_process",
        requires: Some("exec("),
        suppressed_by: None,
        description: "child_process.exec() passes input through a shell — command injection if arguments contain user data.",
        fix: "Use child_process.execFile() or child_process.spawn() with an argument array. Never pass user input to exec().",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-002", cwe: 78, severity: Severity::High,
        category: "Command Injection",
        pattern: "exec(",
        requires: Some("require"),
        suppressed_by: Some("execfile"),
        description: "exec() from child_process with require — likely shell command execution.",
        fix: "Replace with execFile() or spawn() with explicit argument arrays.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-003", cwe: 1321, severity: Severity::High,
        category: "Prototype Pollution",
        pattern: "__proto__",
        requires: None,
        suppressed_by: Some("test"),
        description: "Direct __proto__ access — prototype pollution can lead to denial of service or RCE.",
        fix: "Use Object.create(null) for lookup maps. Validate that keys are not '__proto__', 'constructor', or 'prototype'.",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "NODE-004", cwe: 1321, severity: Severity::High,
        category: "Prototype Pollution",
        pattern: "object.assign(",
        requires: Some("req"),
        suppressed_by: None,
        description: "Object.assign() with request body — prototype pollution if input contains __proto__ key.",
        fix: "Validate/sanitize input keys before merging. Use a schema validator (Zod, Joi) to strip unknown properties.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-005", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "${",
        requires: Some("query"),
        suppressed_by: Some("sanitize"),
        description: "Template literal interpolation in SQL query — SQL injection via string building.",
        fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [id]).",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-006", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "readfile",
        requires: Some("req"),
        suppressed_by: None,
        description: "fs.readFile with request-derived path — path traversal to read arbitrary files.",
        fix: "Use path.resolve() and verify the result starts with the allowed base directory.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-007", cwe: 918, severity: Severity::High,
        category: "SSRF",
        pattern: "fetch(",
        requires: Some("req"),
        suppressed_by: Some("allowlist"),
        description: "fetch() with request-derived URL — server-side request forgery (SSRF).",
        fix: "Validate URLs against an allowlist of permitted hosts. Block internal/private IP ranges.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-008", cwe: 918, severity: Severity::High,
        category: "SSRF",
        pattern: "axios",
        requires: Some("req"),
        suppressed_by: Some("allowlist"),
        description: "axios request with user-controlled URL — SSRF vulnerability.",
        fix: "Validate the target URL against an allowlist. Parse the URL and reject private/internal IP ranges.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-009", cwe: 1333, severity: Severity::Medium,
        category: "ReDoS",
        pattern: "new regexp(",
        requires: Some("req"),
        suppressed_by: None,
        description: "RegExp constructed from user input — ReDoS (catastrophic backtracking) risk.",
        fix: "Never build RegExp from user input. If necessary, use a safe regex library like re2 or safe-regex.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NODE-010", cwe: 601, severity: Severity::Medium,
        category: "Open Redirect",
        pattern: "redirect(",
        requires: Some("req"),
        suppressed_by: Some("allowlist"),
        description: "HTTP redirect with user-controlled URL — open redirect enables phishing attacks.",
        fix: "Validate redirect targets against an allowlist of permitted paths/domains.",
        languages: &["js", "ts"],
        taint_aware: true,
    },

    // ── Category 12: Go-specific (CWE-78, CWE-89, CWE-798) ──────────
    SastRule {
        id: "GO-001", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "db.query(",
        requires: Some("+"),
        suppressed_by: None,
        description: "Go SQL query built with string concatenation — SQL injection vector.",
        fix: "Use parameterized queries: db.Query(\"SELECT * FROM users WHERE id = $1\", id)",
        languages: &["go"],
        taint_aware: false,
    },
    SastRule {
        id: "GO-002", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "fmt.sprintf",
        requires: Some("select"),
        suppressed_by: None,
        description: "SQL query built with fmt.Sprintf — injection via string formatting.",
        fix: "Use parameterized queries with database/sql instead of formatting SQL strings.",
        languages: &["go"],
        taint_aware: false,
    },
    SastRule {
        id: "GO-003", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "exec.command(",
        requires: Some("+"),
        suppressed_by: None,
        description: "os/exec.Command with concatenated input — command injection.",
        fix: "Pass arguments as separate parameters to exec.Command, not as a single concatenated string.",
        languages: &["go"],
        taint_aware: false,
    },
    SastRule {
        id: "GO-004", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "filepath.join(",
        requires: Some("request"),
        suppressed_by: None,
        description: "filepath.Join with request-derived path — path traversal if not validated.",
        fix: "Use filepath.Clean() then verify the path starts with the expected base directory.",
        languages: &["go"],
        taint_aware: true,
    },
    SastRule {
        id: "GO-005", cwe: 295, severity: Severity::High,
        category: "Insecure Config",
        pattern: "insecureskipverify",
        requires: Some("true"),
        suppressed_by: None,
        description: "TLS InsecureSkipVerify=true disables certificate validation — MITM risk.",
        fix: "Remove InsecureSkipVerify. For dev, configure a custom CA cert pool instead.",
        languages: &["go"],
        taint_aware: false,
    },

    // ── Category 13: Java-specific (CWE-89, CWE-611, CWE-502, CWE-918) ─
    SastRule {
        id: "JAVA-001", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "createquery(",
        requires: Some("+"),
        suppressed_by: None,
        description: "JPA/Hibernate createQuery with string concatenation — SQL injection.",
        fix: "Use named parameters: createQuery(\"... WHERE id = :id\").setParameter(\"id\", id)",
        languages: &["java"],
        taint_aware: false,
    },
    SastRule {
        id: "JAVA-002", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "statement.execute",
        requires: Some("+"),
        suppressed_by: None,
        description: "JDBC Statement.execute with concatenation — SQL injection.",
        fix: "Use PreparedStatement with ? placeholders and setString()/setInt() bindings.",
        languages: &["java"],
        taint_aware: false,
    },
    SastRule {
        id: "JAVA-003", cwe: 611, severity: Severity::High,
        category: "XXE",
        pattern: "documentbuilderfactory",
        requires: None,
        suppressed_by: Some("disallow-doctype-decl"),
        description: "XML DocumentBuilderFactory without XXE protection — XML External Entity injection.",
        fix: "Set factory.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true)",
        languages: &["java"],
        taint_aware: false,
    },
    SastRule {
        id: "JAVA-004", cwe: 502, severity: Severity::Critical,
        category: "Unsafe Deserialization",
        pattern: "objectinputstream",
        requires: None,
        suppressed_by: Some("objectinputfilter"),
        description: "Java ObjectInputStream deserialization — RCE via gadget chains (ysoserial).",
        fix: "Avoid ObjectInputStream on untrusted data. Use JSON/protobuf or configure ObjectInputFilter.",
        languages: &["java"],
        taint_aware: false,
    },
    SastRule {
        id: "JAVA-005", cwe: 918, severity: Severity::High,
        category: "SSRF",
        pattern: "new url(",
        requires: Some("request"),
        suppressed_by: Some("allowlist"),
        description: "java.net.URL constructed from request input — SSRF vulnerability.",
        fix: "Validate URLs against an allowlist of permitted hosts. Block internal/private IP ranges.",
        languages: &["java"],
        taint_aware: true,
    },
    SastRule {
        id: "JAVA-006", cwe: 327, severity: Severity::High,
        category: "Insecure Cryptography",
        pattern: "messagedigest.getinstance",
        requires: Some("md5"),
        suppressed_by: None,
        description: "Java MessageDigest using MD5 — broken hash algorithm.",
        fix: "Use MessageDigest.getInstance(\"SHA-256\") or bcrypt for passwords.",
        languages: &["java"],
        taint_aware: false,
    },

    // ── Category 14: Dockerfile SAST (CWE-250, CWE-829) ──────────────
    SastRule {
        id: "DOCKER-001", cwe: 250, severity: Severity::High,
        category: "Container Security",
        pattern: "user root",
        requires: None,
        suppressed_by: None,
        description: "Dockerfile runs as root — container escape gives host root access.",
        fix: "Add 'USER nonroot' or 'USER 1000' before the CMD/ENTRYPOINT instruction.",
        languages: &["dockerfile"],
        taint_aware: false,
    },
    SastRule {
        id: "DOCKER-002", cwe: 829, severity: Severity::Medium,
        category: "Container Security",
        pattern: ":latest",
        requires: Some("from"),
        suppressed_by: None,
        description: "Docker FROM uses :latest tag — builds are non-reproducible and may break.",
        fix: "Pin to a specific image digest or version tag: FROM node:20-alpine@sha256:...",
        languages: &["dockerfile"],
        taint_aware: false,
    },
    SastRule {
        id: "DOCKER-003", cwe: 798, severity: Severity::Critical,
        category: "Hardcoded Secrets",
        pattern: "env ",
        requires: Some("password"),
        suppressed_by: None,
        description: "Secret passed via ENV in Dockerfile — visible in docker inspect and image layers.",
        fix: "Use Docker secrets, build args with --secret, or runtime env injection instead.",
        languages: &["dockerfile"],
        taint_aware: false,
    },
    SastRule {
        id: "DOCKER-004", cwe: 829, severity: Severity::High,
        category: "Container Security",
        pattern: "curl",
        requires: Some("| sh"),
        suppressed_by: None,
        description: "curl piped to shell in Dockerfile — supply chain attack vector.",
        fix: "Download then verify checksum before executing. Use package managers when available.",
        languages: &["dockerfile"],
        taint_aware: false,
    },
    SastRule {
        id: "DOCKER-005", cwe: 829, severity: Severity::Medium,
        category: "Container Security",
        pattern: "copy . .",
        requires: None,
        suppressed_by: Some(".dockerignore"),
        description: "COPY . . without .dockerignore — may copy secrets, .git, node_modules into image.",
        fix: "Create a .dockerignore file excluding .git, .env, node_modules, and other sensitive files.",
        languages: &["dockerfile"],
        taint_aware: false,
    },

    // ── Category 15: Terraform/IaC SAST (CWE-284, CWE-732) ───────────
    SastRule {
        id: "TF-001", cwe: 284, severity: Severity::Critical,
        category: "Infrastructure Security",
        pattern: "0.0.0.0/0",
        requires: None,
        suppressed_by: Some("egress"),
        description: "Security group/firewall rule open to 0.0.0.0/0 — exposed to the entire internet.",
        fix: "Restrict CIDR blocks to specific IPs or VPN ranges. Never use 0.0.0.0/0 for ingress.",
        languages: &["tf", "hcl"],
        taint_aware: false,
    },
    SastRule {
        id: "TF-002", cwe: 732, severity: Severity::Critical,
        category: "Infrastructure Security",
        pattern: "acl",
        requires: Some("public-read"),
        suppressed_by: None,
        description: "S3 bucket ACL set to public-read — data exposed to anonymous internet access.",
        fix: "Remove public ACL. Use bucket policies with specific principal ARNs for access control.",
        languages: &["tf", "hcl"],
        taint_aware: false,
    },
    SastRule {
        id: "TF-003", cwe: 311, severity: Severity::High,
        category: "Infrastructure Security",
        pattern: "encrypted",
        requires: Some("false"),
        suppressed_by: None,
        description: "Storage encryption explicitly disabled — data at rest is unprotected.",
        fix: "Set encrypted = true and configure a KMS key for encryption at rest.",
        languages: &["tf", "hcl"],
        taint_aware: false,
    },
    SastRule {
        id: "TF-004", cwe: 284, severity: Severity::High,
        category: "Infrastructure Security",
        pattern: "\"*\"",
        requires: Some("action"),
        suppressed_by: None,
        description: "IAM policy with Action: \"*\" — grants unrestricted permissions.",
        fix: "Follow least-privilege: specify exact actions needed (e.g., s3:GetObject, s3:PutObject).",
        languages: &["tf", "hcl"],
        taint_aware: false,
    },

    // ── Category 16: Kubernetes SAST (CWE-250, CWE-284) ──────────────
    SastRule {
        id: "K8S-001", cwe: 250, severity: Severity::Critical,
        category: "Container Security",
        pattern: "privileged",
        requires: Some("true"),
        suppressed_by: Some("test"),
        description: "Kubernetes container running in privileged mode — full host access, container escape trivial.",
        fix: "Remove privileged: true. Use specific capabilities (NET_ADMIN, SYS_TIME) if needed.",
        languages: &["k8s"],
        taint_aware: false,
    },
    SastRule {
        id: "K8S-002", cwe: 250, severity: Severity::High,
        category: "Container Security",
        pattern: "hostpath",
        requires: None,
        suppressed_by: Some("test"),
        description: "Kubernetes hostPath volume — exposes host filesystem to container.",
        fix: "Use persistent volume claims (PVC) instead of hostPath for data persistence.",
        languages: &["k8s"],
        taint_aware: false,
    },
    SastRule {
        id: "K8S-003", cwe: 250, severity: Severity::High,
        category: "Container Security",
        pattern: "runasuser",
        requires: Some("0"),
        suppressed_by: None,
        description: "Kubernetes pod running as root (UID 0) — container escape gives host root.",
        fix: "Set runAsUser: 1000 (or any non-root UID) and runAsNonRoot: true in securityContext.",
        languages: &["k8s"],
        taint_aware: false,
    },
    SastRule {
        id: "K8S-004", cwe: 284, severity: Severity::Medium,
        category: "Container Security",
        pattern: "allowprivilegeescalation",
        requires: Some("true"),
        suppressed_by: None,
        description: "allowPrivilegeEscalation: true — process can gain more privileges than its parent.",
        fix: "Set allowPrivilegeEscalation: false in the container securityContext.",
        languages: &["k8s"],
        taint_aware: false,
    },

    // ── Category 17: Shell script SAST (CWE-78, CWE-732) ─────────────
    SastRule {
        id: "SHELL-001", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "eval ",
        requires: None,
        suppressed_by: Some("test"),
        description: "Shell eval — executes arbitrary code, injection risk with any dynamic input.",
        fix: "Eliminate eval. Use arrays and proper quoting for dynamic arguments.",
        languages: &["sh", "bash"],
        taint_aware: false,
    },
    SastRule {
        id: "SHELL-002", cwe: 829, severity: Severity::High,
        category: "Supply Chain",
        pattern: "curl",
        requires: Some("| bash"),
        suppressed_by: None,
        description: "curl piped to bash — remote code execution from untrusted source.",
        fix: "Download the script, verify its checksum/signature, then execute.",
        languages: &["sh", "bash"],
        taint_aware: false,
    },
    SastRule {
        id: "SHELL-003", cwe: 732, severity: Severity::High,
        category: "Insecure Permissions",
        pattern: "chmod 777",
        requires: None,
        suppressed_by: None,
        description: "chmod 777 — world-writable, any user can modify and execute the file.",
        fix: "Use least-privilege permissions: chmod 755 for executables, chmod 644 for data files.",
        languages: &["sh", "bash"],
        taint_aware: false,
    },
    SastRule {
        id: "SHELL-004", cwe: 78, severity: Severity::Medium,
        category: "Command Injection",
        pattern: "$(",
        requires: Some("rm"),
        suppressed_by: None,
        description: "Command substitution with rm — accidental deletion if variable is empty/malformed.",
        fix: "Always quote variables: rm \"${path}\" and validate before deletion.",
        languages: &["sh", "bash"],
        taint_aware: false,
    },

    // ══════════════════════════════════════════════════════════════════
    // P0: C/C++ SAST (CWE-120, CWE-134, CWE-78, CWE-416, CWE-190)
    // ══════════════════════════════════════════════════════════════════

    // ── Buffer overflow (CWE-120) ─────────────────────────────────────
    SastRule {
        id: "CPP-001", cwe: 120, severity: Severity::Critical,
        category: "Buffer Overflow",
        pattern: "strcpy(",
        requires: None,
        suppressed_by: Some("strncpy"),
        description: "strcpy has no bounds checking — buffer overflow if source exceeds destination size.",
        fix: "Use strncpy(dst, src, sizeof(dst)-1) or snprintf(). Better: use std::string in C++.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },
    SastRule {
        id: "CPP-002", cwe: 120, severity: Severity::Critical,
        category: "Buffer Overflow",
        pattern: "gets(",
        requires: None,
        suppressed_by: None,
        description: "gets() has no length limit — guaranteed buffer overflow on long input. Removed in C11.",
        fix: "Use fgets(buf, sizeof(buf), stdin). Never use gets().",
        languages: &["c", "cpp"],
        taint_aware: false,
    },
    SastRule {
        id: "CPP-003", cwe: 120, severity: Severity::High,
        category: "Buffer Overflow",
        pattern: "sprintf(",
        requires: None,
        suppressed_by: Some("snprintf"),
        description: "sprintf has no bounds checking — buffer overflow if formatted output exceeds buffer.",
        fix: "Use snprintf(buf, sizeof(buf), fmt, ...) which truncates safely.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },
    SastRule {
        id: "CPP-004", cwe: 120, severity: Severity::High,
        category: "Buffer Overflow",
        pattern: "strcat(",
        requires: None,
        suppressed_by: Some("strncat"),
        description: "strcat has no bounds checking — overflow if concatenated result exceeds buffer.",
        fix: "Use strncat(dst, src, sizeof(dst)-strlen(dst)-1) or std::string.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },
    SastRule {
        id: "CPP-005", cwe: 120, severity: Severity::Medium,
        category: "Buffer Overflow",
        pattern: "scanf(",
        requires: None,
        suppressed_by: None,
        description: "scanf with %s reads unbounded input — buffer overflow for long input strings.",
        fix: "Use field width: scanf(\"%255s\", buf) or fgets() + sscanf().",
        languages: &["c", "cpp"],
        taint_aware: false,
    },

    // ── Format string (CWE-134) ───────────────────────────────────────
    SastRule {
        id: "CPP-006", cwe: 134, severity: Severity::Critical,
        category: "Format String",
        pattern: "printf(",
        requires: Some("argv"),
        suppressed_by: None,
        description: "printf with user-controlled format string — can read/write arbitrary memory via %n, %x.",
        fix: "Always use a literal format string: printf(\"%s\", user_input) instead of printf(user_input).",
        languages: &["c", "cpp"],
        taint_aware: true,
    },
    SastRule {
        id: "CPP-007", cwe: 134, severity: Severity::High,
        category: "Format String",
        pattern: "syslog(",
        requires: None,
        suppressed_by: None,
        description: "syslog with user-controlled format string — format string attack vector.",
        fix: "Use syslog(LOG_INFO, \"%s\", message) with explicit format string.",
        languages: &["c", "cpp"],
        taint_aware: true,
    },

    // ── Command injection (CWE-78) ────────────────────────────────────
    SastRule {
        id: "CPP-008", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "system(",
        requires: None,
        suppressed_by: Some("test"),
        description: "system() passes command to shell — injection if any argument contains user input.",
        fix: "Use exec family (execvp) with argument arrays. Avoid system() entirely in production code.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },
    SastRule {
        id: "CPP-009", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "popen(",
        requires: None,
        suppressed_by: Some("test"),
        description: "popen() runs command through shell — same injection risk as system().",
        fix: "Use pipe()/fork()/exec() for safe subprocess creation without shell interpretation.",
        languages: &["c", "cpp"],
        taint_aware: true,
    },

    // ── Use-after-free (CWE-416) ──────────────────────────────────────
    SastRule {
        id: "CPP-010", cwe: 416, severity: Severity::High,
        category: "Memory Safety",
        pattern: "free(",
        requires: Some("->"),
        suppressed_by: None,
        description: "Pointer dereference after or near free() — potential use-after-free vulnerability.",
        fix: "Set pointer to NULL after free(). Use smart pointers (unique_ptr, shared_ptr) in C++.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },

    // ── Integer overflow (CWE-190) ────────────────────────────────────
    SastRule {
        id: "CPP-011", cwe: 190, severity: Severity::Medium,
        category: "Integer Overflow",
        pattern: "malloc(",
        requires: Some("*"),
        suppressed_by: Some("check"),
        description: "malloc with multiplication — integer overflow in size calculation can cause heap overflow.",
        fix: "Use calloc(n, size) which checks for overflow, or validate n * size before malloc.",
        languages: &["c", "cpp"],
        taint_aware: false,
    },

    // ══════════════════════════════════════════════════════════════════
    // P0: Swift/iOS SAST (CWE-918, CWE-22, CWE-312, CWE-502, CWE-79)
    // ══════════════════════════════════════════════════════════════════

    SastRule {
        id: "SWIFT-001", cwe: 918, severity: Severity::High,
        category: "SSRF",
        pattern: "urlsession",
        requires: Some("url(string"),
        suppressed_by: Some("allowlist"),
        description: "URLSession with dynamic URL construction — SSRF if URL contains user input.",
        fix: "Validate URLs against an allowlist. Use URLComponents to build URLs safely.",
        languages: &["swift"],
        taint_aware: true,
    },
    SastRule {
        id: "SWIFT-002", cwe: 22, severity: Severity::High,
        category: "Path Traversal",
        pattern: "filemanager",
        requires: Some("contentsoffile"),
        suppressed_by: None,
        description: "FileManager file read with potentially user-controlled path — path traversal risk.",
        fix: "Sanitize paths: resolve symlinks, verify the path starts with the app sandbox directory.",
        languages: &["swift"],
        taint_aware: true,
    },
    SastRule {
        id: "SWIFT-003", cwe: 312, severity: Severity::High,
        category: "Insecure Storage",
        pattern: "userdefaults",
        requires: Some("password"),
        suppressed_by: Some("keychain"),
        description: "UserDefaults used to store passwords/tokens — data is stored in plaintext plist files.",
        fix: "Use Keychain Services for sensitive data: SecItemAdd/SecItemCopyMatching.",
        languages: &["swift"],
        taint_aware: false,
    },
    SastRule {
        id: "SWIFT-004", cwe: 312, severity: Severity::Medium,
        category: "Insecure Storage",
        pattern: "userdefaults",
        requires: Some("token"),
        suppressed_by: Some("keychain"),
        description: "UserDefaults used to store auth tokens — accessible by any tweak on jailbroken devices.",
        fix: "Use Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly for auth tokens.",
        languages: &["swift"],
        taint_aware: false,
    },
    SastRule {
        id: "SWIFT-005", cwe: 502, severity: Severity::High,
        category: "Insecure Deserialization",
        pattern: "nskeyedunarchiver",
        requires: None,
        suppressed_by: Some("requiressecurecoding"),
        description: "NSKeyedUnarchiver without secure coding — arbitrary object instantiation from untrusted data.",
        fix: "Use NSSecureCoding: NSKeyedUnarchiver.unarchivedObject(ofClass:from:) with requiresSecureCoding = true.",
        languages: &["swift"],
        taint_aware: false,
    },
    SastRule {
        id: "SWIFT-006", cwe: 295, severity: Severity::Critical,
        category: "TLS/SSL",
        pattern: "allowsanyhtttps",
        requires: None,
        suppressed_by: None,
        description: "App Transport Security (ATS) bypass — allows insecure HTTP connections.",
        fix: "Remove NSAllowsArbitraryLoads from Info.plist. Use HTTPS with valid certificates.",
        languages: &["swift"],
        taint_aware: false,
    },
    SastRule {
        id: "SWIFT-007", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "loadhtmlstring",
        requires: None,
        suppressed_by: Some("sanitize"),
        description: "WKWebView loadHTMLString with dynamic content — XSS if HTML contains user input.",
        fix: "HTML-encode all user input before embedding in loadHTMLString. Use Content-Security-Policy.",
        languages: &["swift"],
        taint_aware: true,
    },

    // ══════════════════════════════════════════════════════════════════
    // P1: C#/.NET-specific SAST (CWE-918, CWE-502, CWE-78, CWE-89)
    // ══════════════════════════════════════════════════════════════════

    SastRule {
        id: "CS-001", cwe: 918, severity: Severity::High,
        category: "SSRF",
        pattern: "httpclient",
        requires: Some("getasync"),
        suppressed_by: Some("allowlist"),
        description: "HttpClient.GetAsync with user-controlled URL — SSRF to internal services.",
        fix: "Validate URLs against an allowlist. Block private IP ranges (10.x, 172.16-31.x, 192.168.x).",
        languages: &["cs"],
        taint_aware: true,
    },
    SastRule {
        id: "CS-002", cwe: 502, severity: Severity::Critical,
        category: "Insecure Deserialization",
        pattern: "binaryformatter",
        requires: None,
        suppressed_by: None,
        description: "BinaryFormatter deserializes arbitrary types — RCE via crafted payload. Deprecated in .NET 5+.",
        fix: "Use System.Text.Json or JsonSerializer. BinaryFormatter is fundamentally insecure.",
        languages: &["cs"],
        taint_aware: false,
    },
    SastRule {
        id: "CS-003", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "process.start",
        requires: None,
        suppressed_by: None,
        description: "Process.Start with user input — command injection if arguments are not sanitized.",
        fix: "Use ProcessStartInfo with UseShellExecute=false and pass arguments as separate parameters.",
        languages: &["cs"],
        taint_aware: true,
    },
    SastRule {
        id: "CS-004", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "sqlcommand",
        requires: Some("+"),
        suppressed_by: Some("parameters"),
        description: "SqlCommand with string concatenation — SQL injection in ADO.NET.",
        fix: "Use SqlParameter: cmd.Parameters.AddWithValue(\"@id\", userId).",
        languages: &["cs"],
        taint_aware: true,
    },
    SastRule {
        id: "CS-005", cwe: 1333, severity: Severity::Medium,
        category: "ReDoS",
        pattern: "new regex(",
        requires: Some("req"),
        suppressed_by: Some("timeout"),
        description: "Regex constructed from user input — ReDoS (catastrophic backtracking) in .NET regex engine.",
        fix: "Use Regex with RegexOptions.NonBacktracking (.NET 7+) or set MatchTimeout.",
        languages: &["cs"],
        taint_aware: true,
    },
    SastRule {
        id: "CS-006", cwe: 502, severity: Severity::High,
        category: "Insecure Deserialization",
        pattern: "jsonconvert.deserializeobject",
        requires: Some("typenamehandling"),
        suppressed_by: None,
        description: "Newtonsoft.Json with TypeNameHandling — RCE via polymorphic deserialization gadgets.",
        fix: "Use TypeNameHandling.None (default) or use System.Text.Json which has no type name handling.",
        languages: &["cs"],
        taint_aware: false,
    },

    // ══════════════════════════════════════════════════════════════════
    // P2: PHP SAST (CWE-89, CWE-78, CWE-94, CWE-98, CWE-79)
    // ══════════════════════════════════════════════════════════════════

    SastRule {
        id: "PHP-001", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "mysql_query(",
        requires: Some("$_"),
        suppressed_by: Some("prepared"),
        description: "mysql_query with superglobal ($_GET/$_POST) — direct SQL injection.",
        fix: "Use PDO prepared statements: $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-002", cwe: 89, severity: Severity::Critical,
        category: "SQL Injection",
        pattern: "->query(",
        requires: Some("$_"),
        suppressed_by: Some("prepare"),
        description: "mysqli->query with user input — SQL injection via string interpolation.",
        fix: "Use $stmt = $mysqli->prepare('...'); $stmt->bind_param('s', $param);",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-003", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "exec(",
        requires: Some("$_"),
        suppressed_by: Some("escapeshellarg"),
        description: "exec() with user input — shell command injection.",
        fix: "Use escapeshellarg() on each argument. Better: avoid exec() and use PHP-native alternatives.",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-004", cwe: 78, severity: Severity::Critical,
        category: "Command Injection",
        pattern: "system(",
        requires: Some("$_"),
        suppressed_by: Some("escapeshellarg"),
        description: "system() with user input — direct shell command injection.",
        fix: "Use escapeshellarg() + escapeshellcmd(). Prefer PHP-native functions over shell commands.",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-005", cwe: 94, severity: Severity::Critical,
        category: "Code Injection",
        pattern: "eval(",
        requires: None,
        suppressed_by: Some("test"),
        description: "eval() executes arbitrary PHP — code injection if input contains user data.",
        fix: "Never use eval(). Restructure code to use arrays, callbacks, or proper templating.",
        languages: &["php"],
        taint_aware: false,
    },
    SastRule {
        id: "PHP-006", cwe: 98, severity: Severity::Critical,
        category: "File Inclusion",
        pattern: "include(",
        requires: Some("$_"),
        suppressed_by: None,
        description: "include() with user-controlled path — Local/Remote File Inclusion (LFI/RFI).",
        fix: "Use a whitelist of allowed files. Never pass user input to include/require.",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-007", cwe: 98, severity: Severity::Critical,
        category: "File Inclusion",
        pattern: "require(",
        requires: Some("$_"),
        suppressed_by: None,
        description: "require() with user-controlled path — Local/Remote File Inclusion.",
        fix: "Use a whitelist of allowed files. Never pass user input to include/require.",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-008", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "echo $_",
        requires: None,
        suppressed_by: Some("htmlspecialchars"),
        description: "Echoing superglobal directly — reflected XSS vulnerability.",
        fix: "Always escape output: echo htmlspecialchars($_GET['input'], ENT_QUOTES, 'UTF-8');",
        languages: &["php"],
        taint_aware: true,
    },
    SastRule {
        id: "PHP-009", cwe: 502, severity: Severity::High,
        category: "Insecure Deserialization",
        pattern: "unserialize(",
        requires: None,
        suppressed_by: Some("allowed_classes"),
        description: "unserialize() instantiates arbitrary objects — RCE via PHP object injection (POP chains).",
        fix: "Use json_decode() for data. If unserialize is required, pass ['allowed_classes' => false].",
        languages: &["php"],
        taint_aware: false,
    },

    // ══════════════════════════════════════════════════════════════════
    // Frontend Framework SAST: Vue / Angular / Svelte / HTML Templates
    // (CWE-79, CWE-94, CWE-116)
    // ══════════════════════════════════════════════════════════════════

    // ── Vue.js XSS ────────────────────────────────────────────────────
    SastRule {
        id: "VUE-001", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "v-html",
        requires: None,
        suppressed_by: Some("sanitize"),
        description: "Vue v-html directive renders raw HTML — XSS if bound to user-controlled data.",
        fix: "Use {{ interpolation }} for text content (auto-escaped). If v-html is needed, sanitize with DOMPurify first.",
        languages: &["vue", "js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "VUE-002", cwe: 601, severity: Severity::Medium,
        category: "Open Redirect",
        pattern: "v-bind:href",
        requires: None,
        suppressed_by: Some("allowlist"),
        description: "Vue dynamic href binding — open redirect or javascript: URI XSS if user-controlled.",
        fix: "Validate URLs against an allowlist. Block javascript: and data: URI schemes.",
        languages: &["vue", "js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "VUE-003", cwe: 79, severity: Severity::Medium,
        category: "XSS",
        pattern: ":href",
        requires: Some("$route"),
        suppressed_by: Some("sanitize"),
        description: "Vue dynamic href with route parameter — XSS via crafted route params.",
        fix: "Validate route parameters before binding to href. Use router-link component instead.",
        languages: &["vue", "js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "VUE-004", cwe: 94, severity: Severity::Critical,
        category: "Template Injection",
        pattern: "v-bind:is",
        requires: None,
        suppressed_by: None,
        description: "Vue dynamic component with v-bind:is — component injection if value is user-controlled.",
        fix: "Whitelist allowed component names. Never pass user input directly to :is binding.",
        languages: &["vue", "js", "ts"],
        taint_aware: false,
    },

    // ── Angular XSS / Template Injection ──────────────────────────────
    SastRule {
        id: "NG-001", cwe: 79, severity: Severity::Critical,
        category: "XSS",
        pattern: "bypasssecuritytrust",
        requires: None,
        suppressed_by: None,
        description: "Angular DomSanitizer bypass — disables Angular's built-in XSS protection.",
        fix: "Avoid bypassSecurityTrust*. If needed, sanitize input with DOMPurify BEFORE bypassing.",
        languages: &["ts", "js"],
        taint_aware: false,
    },
    SastRule {
        id: "NG-002", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "[innerhtml]",
        requires: None,
        suppressed_by: Some("sanitize"),
        description: "Angular [innerHTML] binding — bypasses template auto-escaping.",
        fix: "Use Angular's built-in sanitization or DomSanitizer. Prefer text interpolation {{ }} for text content.",
        languages: &["ts", "js", "html"],
        taint_aware: false,
    },
    SastRule {
        id: "NG-003", cwe: 94, severity: Severity::Critical,
        category: "Template Injection",
        pattern: "$compile(",
        requires: None,
        suppressed_by: None,
        description: "AngularJS $compile with user input — server-side template injection (SSTI) in AngularJS.",
        fix: "Never $compile user-controlled strings. Migrate from AngularJS to Angular (v2+) which doesn't have $compile.",
        languages: &["js", "ts"],
        taint_aware: true,
    },
    SastRule {
        id: "NG-004", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "ng-bind-html",
        requires: None,
        suppressed_by: Some("$sce"),
        description: "AngularJS ng-bind-html renders raw HTML — XSS if content is user-controlled.",
        fix: "Use $sce.trustAsHtml() only with sanitized content. Prefer ng-bind for text.",
        languages: &["html", "js"],
        taint_aware: false,
    },
    SastRule {
        id: "NG-005", cwe: 79, severity: Severity::Medium,
        category: "XSS",
        pattern: "elementref",
        requires: Some("nativeelement"),
        suppressed_by: None,
        description: "Angular ElementRef.nativeElement direct DOM access — bypasses Angular's sanitization.",
        fix: "Use Renderer2 instead of direct DOM access via ElementRef.nativeElement.",
        languages: &["ts"],
        taint_aware: false,
    },

    // ── Svelte XSS ────────────────────────────────────────────────────
    SastRule {
        id: "SVELTE-001", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "{@html",
        requires: None,
        suppressed_by: Some("sanitize"),
        description: "Svelte {@html} tag renders raw HTML — XSS if content is user-controlled.",
        fix: "Sanitize with DOMPurify before {@html}: {@html DOMPurify.sanitize(content)}",
        languages: &["svelte", "js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "SVELTE-002", cwe: 79, severity: Severity::Medium,
        category: "XSS",
        pattern: "bind:innerhtml",
        requires: None,
        suppressed_by: Some("sanitize"),
        description: "Svelte bind:innerHTML — two-way binding with raw HTML, XSS risk.",
        fix: "Avoid bind:innerHTML. Use textContent binding or sanitize HTML content.",
        languages: &["svelte"],
        taint_aware: false,
    },

    // ── HTML Template Security ────────────────────────────────────────
    SastRule {
        id: "HTML-001", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "onerror=",
        requires: None,
        suppressed_by: Some("test"),
        description: "Inline onerror handler — classic XSS vector in HTML templates.",
        fix: "Remove inline event handlers. Use addEventListener() in JavaScript instead.",
        languages: &["html", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "HTML-002", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "onclick=",
        requires: Some("\""),
        suppressed_by: None,
        description: "Inline onclick with dynamic content — XSS if value contains user input.",
        fix: "Remove inline handlers. Use addEventListener() with proper escaping.",
        languages: &["html"],
        taint_aware: false,
    },
    SastRule {
        id: "HTML-003", cwe: 79, severity: Severity::Critical,
        category: "XSS",
        pattern: "javascript:",
        requires: None,
        suppressed_by: Some("test"),
        description: "javascript: URI scheme — direct XSS execution vector in href/src attributes.",
        fix: "Never use javascript: URIs. Use event listeners or proper navigation patterns.",
        languages: &["html", "js", "ts", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "HTML-004", cwe: 16, severity: Severity::Medium,
        category: "Security Misconfiguration",
        pattern: "target=\"_blank\"",
        requires: None,
        suppressed_by: Some("noopener"),
        description: "target=\"_blank\" without rel=\"noopener\" — tabnabbing vulnerability.",
        fix: "Add rel=\"noopener noreferrer\" to all target=\"_blank\" links.",
        languages: &["html", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "HTML-005", cwe: 346, severity: Severity::Medium,
        category: "Security Misconfiguration",
        pattern: "allow=\"*\"",
        requires: Some("iframe"),
        suppressed_by: None,
        description: "iframe with unrestricted allow policy — grants embedded page full permissions.",
        fix: "Restrict iframe permissions: allow=\"camera; microphone\" and add sandbox attribute.",
        languages: &["html", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "HTML-006", cwe: 79, severity: Severity::High,
        category: "XSS",
        pattern: "srcdoc=",
        requires: None,
        suppressed_by: Some("sandbox"),
        description: "iframe srcdoc with dynamic content — XSS if HTML is user-controlled.",
        fix: "Sanitize srcdoc content with DOMPurify. Add sandbox attribute to restrict capabilities.",
        languages: &["html", "vue", "svelte", "js", "ts"],
        taint_aware: false,
    },

    // ── CSS Injection (CWE-79 via CSS) ────────────────────────────────
    SastRule {
        id: "CSS-001", cwe: 79, severity: Severity::Critical,
        category: "CSS Injection",
        pattern: "expression(",
        requires: None,
        suppressed_by: None,
        description: "CSS expression() — executes JavaScript in IE. Legacy but still exploitable.",
        fix: "Remove CSS expression(). Use JavaScript event handlers or CSS animations instead.",
        languages: &["css", "html", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "CSS-002", cwe: 79, severity: Severity::Critical,
        category: "CSS Injection",
        pattern: "url(javascript:",
        requires: None,
        suppressed_by: None,
        description: "CSS url() with javascript: scheme — XSS via CSS properties (background, list-style).",
        fix: "Never use javascript: in CSS url(). Use only https:// or relative paths.",
        languages: &["css", "html", "vue", "svelte"],
        taint_aware: false,
    },
    SastRule {
        id: "CSS-003", cwe: 79, severity: Severity::High,
        category: "CSS Injection",
        pattern: "-moz-binding",
        requires: None,
        suppressed_by: None,
        description: "-moz-binding can load XBL bindings with JavaScript — XSS in Firefox.",
        fix: "Remove -moz-binding. It is deprecated and a known XSS vector.",
        languages: &["css", "html"],
        taint_aware: false,
    },
    SastRule {
        id: "CSS-004", cwe: 79, severity: Severity::High,
        category: "CSS Injection",
        pattern: "behavior:",
        requires: Some("url("),
        suppressed_by: None,
        description: "CSS behavior property loads HTC files with JavaScript — XSS in IE.",
        fix: "Remove behavior property. Use modern CSS and JavaScript alternatives.",
        languages: &["css", "html"],
        taint_aware: false,
    },
    SastRule {
        id: "CSS-005", cwe: 79, severity: Severity::Medium,
        category: "CSS Injection",
        pattern: "style=",
        requires: Some("${"),
        suppressed_by: Some("sanitize"),
        description: "Inline style with template interpolation — CSS injection if user-controlled.",
        fix: "Use CSS-in-JS libraries with auto-escaping, or sanitize style values against an allowlist.",
        languages: &["js", "ts", "html", "vue", "svelte"],
        taint_aware: false,
    },

    // ── Frontend Framework Config Security ─────────────────────────────
    SastRule {
        id: "FE-001", cwe: 352, severity: Severity::High,
        category: "CSRF",
        pattern: "withcredentials",
        requires: Some("true"),
        suppressed_by: Some("csrf"),
        description: "Cross-origin request with credentials — CSRF if target doesn't validate origin.",
        fix: "Implement CSRF tokens. Validate Origin/Referer headers. Use SameSite cookies.",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "FE-002", cwe: 922, severity: Severity::High,
        category: "Insecure Storage",
        pattern: "localstorage.setitem",
        requires: Some("token"),
        suppressed_by: None,
        description: "Auth token stored in localStorage — accessible via XSS, never expires.",
        fix: "Store tokens in httpOnly cookies (immune to XSS). Use sessionStorage for session-scoped data.",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "FE-003", cwe: 922, severity: Severity::High,
        category: "Insecure Storage",
        pattern: "localstorage.setitem",
        requires: Some("password"),
        suppressed_by: None,
        description: "Password stored in localStorage — plaintext credential exposure via XSS.",
        fix: "Never store passwords client-side. Use server-side session management.",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "FE-004", cwe: 319, severity: Severity::Medium,
        category: "Insecure Transport",
        pattern: "postmessage(",
        requires: Some("*"),
        suppressed_by: Some("origin"),
        description: "postMessage with '*' origin — any window can receive sensitive data.",
        fix: "Specify exact target origin: window.postMessage(data, 'https://trusted.com')",
        languages: &["js", "ts"],
        taint_aware: false,
    },
    SastRule {
        id: "FE-005", cwe: 346, severity: Severity::High,
        category: "Origin Validation",
        pattern: "addeventlistener",
        requires: Some("message"),
        suppressed_by: Some("origin"),
        description: "Window message listener without origin check — any page can send messages.",
        fix: "Always validate event.origin: if (event.origin !== 'https://trusted.com') return;",
        languages: &["js", "ts"],
        taint_aware: false,
    },
];

// ═══════════════════════════════════════════════════════════════════
// Language detection
// ═══════════════════════════════════════════════════════════════════

fn detect_lang(source: &str) -> Option<&'static str> {
    let lower = source.to_lowercase();
    let basename = lower.rsplit(['/', '\\']).next().unwrap_or(&lower);
    if lower.ends_with(".py") || lower.ends_with(".pyw") {
        Some("py")
    } else if lower.ends_with(".rs") {
        Some("rs")
    } else if lower.ends_with(".js")
        || lower.ends_with(".jsx")
        || lower.ends_with(".mjs")
        || lower.ends_with(".cjs")
    {
        Some("js")
    } else if lower.ends_with(".ts")
        || lower.ends_with(".tsx")
        || lower.ends_with(".mts")
        || lower.ends_with(".cts")
    {
        Some("ts")
    } else if lower.ends_with(".go") {
        Some("go")
    } else if lower.ends_with(".java") || lower.ends_with(".kt") {
        Some("java")
    } else if lower.ends_with(".cs") || lower.ends_with(".csx") {
        Some("cs")
    } else if lower.ends_with(".swift") {
        Some("swift")
    } else if lower.ends_with(".c") || lower.ends_with(".h") {
        Some("c")
    } else if lower.ends_with(".cpp")
        || lower.ends_with(".cc")
        || lower.ends_with(".hpp")
        || lower.ends_with(".hxx")
    {
        Some("cpp")
    } else if lower.ends_with(".php") {
        Some("php")
    } else if lower.ends_with(".rb") {
        Some("rb")
    } else if lower.ends_with(".sh") || lower.ends_with(".bash") || lower.ends_with(".zsh") {
        Some("sh")
    } else if basename.starts_with("dockerfile") {
        Some("dockerfile")
    } else if lower.ends_with(".tf") || lower.ends_with(".hcl") {
        Some("tf")
    } else if lower.ends_with(".vue") {
        Some("vue")
    } else if lower.ends_with(".svelte") {
        Some("svelte")
    } else if lower.ends_with(".html") || lower.ends_with(".htm") {
        Some("html")
    } else if lower.ends_with(".css") || lower.ends_with(".scss") || lower.ends_with(".less") {
        Some("css")
    } else {
        None
    }
}

fn rule_applies(rule: &SastRule, lang: Option<&str>) -> bool {
    if rule.languages.is_empty() {
        return true;
    }
    match lang {
        Some(l) => {
            if rule.languages.contains(&l) {
                return true;
            }
            // C# is structurally similar to Java — Java rules also apply
            if l == "cs" && rule.languages.contains(&"java") {
                return true;
            }
            // C files match C++ rules (C++ is a superset of C for security purposes)
            if l == "c" && rule.languages.contains(&"cpp") {
                return true;
            }
            // Bash is a superset of sh
            if l == "bash" && rule.languages.contains(&"sh") {
                return true;
            }
            // Vue/Svelte SFCs embed JavaScript — JS/TS rules apply
            if (l == "vue" || l == "svelte")
                && (rule.languages.contains(&"js") || rule.languages.contains(&"ts"))
            {
                return true;
            }
            false
        }
        None => false,
    }
}

/// Detect non-code files where structural security rules are meaningless.
/// Markdown relative links (`../guide.md`), HTML hrefs, CSS paths, etc.
/// are NOT security vulnerabilities — scanning them produces false positives
/// that erode trust in the dashboard.
fn is_non_code_file(source: &str) -> bool {
    let lower = source.to_lowercase();
    // Note: .html, .css are NOT non-code — they have dedicated SAST rules
    lower.ends_with(".md")
        || lower.ends_with(".txt")
        || lower.ends_with(".rst")
        || lower.ends_with(".svg")
        || lower.ends_with(".xml")
        || lower.ends_with(".json")
        || lower.ends_with(".yaml")
        || lower.ends_with(".yml")
        || lower.ends_with(".toml")
        || lower.ends_with(".cfg")
        || lower.ends_with(".ini")
        || lower.ends_with(".csv")
}

// ═══════════════════════════════════════════════════════════════════
// Taint-flow simulation
/// Inspired by IRIS (ICLR 2025): track user-controlled sources across lines.
/// This is a lightweight single-function approximation — no full dataflow graph.
/// Sources: function parameters named after common input patterns,
///          request.*, form.*, args.*, query.*, input(), sys.argv.
/// Sinks: the taint-aware rules above that fire only when a tainted variable
///        appears on the same line as the dangerous pattern.
// ═══════════════════════════════════════════════════════════════════
static TAINT_SOURCES: &[&str] = &[
    "request.",
    "req.",
    "form.",
    "args.",
    "kwargs.",
    "query.",
    "params.",
    "body.",
    "data[",
    "input(",
    "sys.argv",
    "os.environ",
    "environ.get(",
    "getenv(",
    "document.",
    "location.",
    "window.location",
    "event.target.value",
    "e.target.value",
    // Node.js / Express / Koa / Fastify
    "req.body",
    "req.query",
    "req.params",
    "req.headers",
    "ctx.request",
    "ctx.params",
    "ctx.query",
    "process.argv",
    // C/C++ — command-line and environment input
    "argv[",
    "argv)",
    "argc",
    "getenv(",
    "fgets(",
    "fread(",
    "recv(",
    "recvfrom(",
    "read(",
    "scanf(",
    "gets(",
    "getline(",
    // PHP superglobals — all user-controlled
    "$_get",
    "$_post",
    "$_request",
    "$_cookie",
    "$_server",
    "$_files",
    "$_env",
    "$_session",
    // C# / .NET — request input
    "request.query",
    "request.form",
    "request.body",
    "request.headers",
    "httpcontext",
    // Java — servlet request
    "getparameter(",
    "getquerystring(",
    "getheader(",
    "getinputstream(",
    "getreader(",
    // Swift/iOS — URL and user input
    "urlcomponents",
    "url(string",
    "uipasteboard",
    "uitextfield",
    "uidocumentpicker",
    // Go — request input
    "r.formvalue(",
    "r.url.query(",
    "r.body",
    "r.header.get(",
    "r.postform",
];

static TAINT_PROPAGATORS: &[&str] = &[
    " = ",
    ".get(",
    ".pop(",
    ".strip(",
    ".lower(",
    ".upper(",
    ".split(",
    ".replace(",
    ".decode(",
];

/// Collect variable names that hold tainted (user-controlled) values.
fn collect_taint_sources(lines: &[&str]) -> HashMap<usize, Vec<String>> {
    let mut tainted_vars: HashMap<usize, Vec<String>> = HashMap::new();

    for (idx, &line) in lines.iter().enumerate() {
        let lower = line.to_lowercase();

        // Direct taint source reference
        for &src in TAINT_SOURCES {
            if lower.contains(src) {
                // Extract LHS variable name if this is an assignment
                if let Some(var) = extract_assignment_lhs(line) {
                    tainted_vars.entry(idx).or_default().push(var);
                }
                // The whole line is tainted regardless
                tainted_vars.entry(idx).or_default().push("<line>".into());
            }
        }
    }
    tainted_vars
}

/// Extract the left-hand side variable name from a simple assignment.
/// Works for: `var_name = ...`, `var_name: Type = ...`
fn extract_assignment_lhs(line: &str) -> Option<String> {
    let trimmed = line.trim();
    // Find first `=` that is not `==`, `!=`, `<=`, `>=`
    let bytes = trimmed.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b'=' {
            // Check it's not ==, !=, <=, >=
            let prev = if i > 0 { bytes[i - 1] } else { 0 };
            let next = if i < bytes.len() - 1 { bytes[i + 1] } else { 0 };
            if next == b'=' || prev == b'!' || prev == b'<' || prev == b'>' || prev == b'=' {
                continue;
            }
            // LHS is everything before the = (ensure safe UTF-8 boundary)
            if !trimmed.is_char_boundary(i) {
                continue;
            }
            let lhs = trimmed[..i].trim();
            // Strip type annotation if present (Python: `var: Type`)
            let var_name = lhs.split(':').next()?.trim();
            // Strip common language prefixes: var, let, const, auto, char*, int, etc.
            // Take the LAST whitespace-separated token as the variable name
            let var_name = var_name.split_whitespace().last().unwrap_or(var_name);
            // Strip pointer/reference markers (C/C++: *ptr, &ref)
            let var_name = var_name.trim_start_matches(['*', '&']);
            // Strip PHP $ prefix for matching purposes
            let var_name = var_name.trim_start_matches('$');
            // Only return simple identifiers
            if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') && !var_name.is_empty() {
                return Some(var_name.to_ascii_lowercase());
            }
            return None;
        }
    }
    None
}

/// Given source lines and the set of taint sources, propagate taint
/// through assignments. Returns a set of tainted variable names as of
/// each line, plus which line they were last updated.
///
/// Algorithm: single-pass forward propagation.
/// Complexity: O(N × V) where V = number of tainted variables (typically < 20).
fn propagate_taint(
    lines: &[&str],
    direct_sources: &HashMap<usize, Vec<String>>,
) -> HashSet<String> {
    let mut tainted: HashSet<String> = HashSet::new();

    // Seed with directly-sourced variables
    for vars in direct_sources.values() {
        for v in vars {
            if v != "<line>" {
                tainted.insert(v.clone());
            }
        }
    }

    // Propagate: if RHS contains a tainted variable, LHS becomes tainted
    for line in lines {
        let lower = line.to_lowercase();
        let has_propagator = TAINT_PROPAGATORS.iter().any(|&p| lower.contains(p));
        if !has_propagator {
            continue;
        }
        // Check if any tainted variable appears on the RHS
        let rhs_tainted = tainted.iter().any(|var| lower.contains(var.as_str()));
        if rhs_tainted {
            if let Some(lhs) = extract_assignment_lhs(line) {
                tainted.insert(lhs);
            }
        }
    }
    tainted
}

/// Check if a line refers to any tainted variable.
fn line_is_tainted(
    line_lower: &str,
    tainted_vars: &HashSet<String>,
    direct_sources: &HashMap<usize, Vec<String>>,
    line_idx: usize,
) -> bool {
    // Direct source on this exact line
    if let Some(vars) = direct_sources.get(&line_idx) {
        if vars.iter().any(|v| v == "<line>") {
            return true;
        }
    }
    // Tainted variable appears in this line
    tainted_vars
        .iter()
        .any(|var| line_lower.contains(var.as_str()))
}

// ═══════════════════════════════════════════════════════════════════
// False-positive suppression
// ═══════════════════════════════════════════════════════════════════

/// Detect if a line is inside a comment block.
struct CommentTracker {
    in_block_comment: bool,
}

impl CommentTracker {
    fn new() -> Self {
        CommentTracker {
            in_block_comment: false,
        }
    }

    fn update_and_check(&mut self, line: &str) -> bool {
        let trimmed = line.trim();

        // Block comment start/end
        if self.in_block_comment {
            if trimmed.contains("*/") || trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''")
            {
                self.in_block_comment = false;
            }
            return true; // Inside block comment
        }

        // Start of block comment
        if trimmed.starts_with("/*") || trimmed.starts_with("/**") {
            self.in_block_comment = !trimmed.contains("*/");
            return true;
        }
        if (trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''"))
            && trimmed.len() > 3
            && !trimmed[3..].contains("\"\"\"")
            && !trimmed[3..].contains("'''")
        {
            self.in_block_comment = true;
            return true;
        }

        // Single-line comment
        if trimmed.starts_with("//") || trimmed.starts_with('#') {
            return true;
        }

        false
    }
}

/// Confidence modifier based on context.
fn confidence_for_context(source: &str, line: &str, rule: &SastRule) -> f64 {
    let source_lower = source.to_lowercase();
    let line_lower = line.to_lowercase();

    let mut conf = 1.0_f64;

    // Test files: lower confidence (findings likely intentional/synthetic)
    if source_lower.contains("test_")
        || source_lower.contains("_test.")
        || source_lower.contains("spec.")
        || source_lower.contains("mock")
    {
        conf *= 0.4;
    }

    // Inline suppression comment
    if line_lower.contains("nosec")
        || line_lower.contains("noqa")
        || line_lower.contains("sast-ignore")
    {
        conf *= 0.1;
    }

    // String literal context (variable name matches but it's in a comment or docstring)
    if line_lower.trim_start().starts_with('#') || line_lower.trim_start().starts_with("//") {
        conf *= 0.1;
    }

    // Taint-aware rules get a confidence boost when triggered (IRIS insight)
    if rule.taint_aware {
        conf = (conf * 1.2).min(1.0);
    }

    conf.max(0.05)
}

// ═══════════════════════════════════════════════════════════════════
// CVSS-inspired aggregate risk score
///
/// Formula inspired by CVSS v3.1 base score:
///   risk = min(10, Σ(severity_weight × confidence × (1 + taint_bonus)) / scaling_factor)
///
/// Rationale:
///   - Each Critical adds ~2.4 to the score
///   - Each High adds ~1.3
///   - Confidence-weighted so low-confidence findings don't dominate
///   - Capped at 10.0 (CVSS maximum)
// ═══════════════════════════════════════════════════════════════════
fn compute_risk_score(findings: &[SastFinding]) -> f64 {
    if findings.is_empty() {
        return 0.0;
    }

    let raw: f64 = findings
        .iter()
        .map(|f| {
            let taint_boost = if f.taint_flow { 1.3 } else { 1.0 };
            f.severity.cvss_weight() * f.confidence * taint_boost
        })
        .sum();

    // Logarithmic compression to match CVSS's non-linear scale
    // At raw=4 (one Critical + confidence 1.0) → score ~7.0
    let compressed = 10.0 * (1.0 - (-raw / 4.0).exp());
    compressed.min(10.0)
}

// ═══════════════════════════════════════════════════════════════════
// Main scan function
// ═══════════════════════════════════════════════════════════════════

/// Scan `content` from `source` file and return a full SastReport.
///
/// This is the primary entry point. Call once per `ingest()`.
pub fn scan_content(content: &str, source: &str) -> SastReport {
    let lang = detect_lang(source);
    let is_non_code = is_non_code_file(source);
    let lines: Vec<&str> = content.lines().collect();

    // Taint analysis (IRIS-inspired): one pass to collect sources, one to propagate
    let direct_sources = collect_taint_sources(&lines);
    let tainted_vars = propagate_taint(&lines, &direct_sources);

    let mut findings: Vec<SastFinding> = Vec::new();
    let mut comment_tracker = CommentTracker::new();

    for (idx, &line) in lines.iter().enumerate() {
        let line_number = idx + 1;
        let is_comment = comment_tracker.update_and_check(line);

        // Only suppress structural patterns, not literal secret scans.
        // Secrets can appear in comments (e.g., commented-out credentials).

        let line_lower = line.to_lowercase();

        for rule in RULES {
            // Language filter
            if !rule_applies(rule, lang) {
                continue;
            }

            // Non-code files: skip structural rules (path traversal, XSS, etc.)
            // Keep CWE-798 (hardcoded secrets) — credentials leak in docs too
            if is_non_code && rule.languages.is_empty() && !rule.taint_aware && rule.cwe != 798 {
                continue;
            }

            // Pattern match (case-insensitive)
            if !line_lower.contains(rule.pattern) {
                continue;
            }

            // requires: secondary pattern must also be present
            if let Some(req) = rule.requires {
                if !line_lower.contains(&req.to_lowercase()[..]) {
                    continue;
                }
            }

            // suppressed_by: if this pattern is present on this line, don't fire
            // For MEM-001: also check the previous and next lines (SAFETY comments often precede unsafe)
            if let Some(supp) = rule.suppressed_by {
                let supp_lower = supp.to_lowercase();
                let on_this_line = line_lower.contains(&supp_lower[..]);
                let on_prev_line = idx
                    .checked_sub(1)
                    .and_then(|pi| lines.get(pi))
                    .map(|pl| pl.to_lowercase().contains(&supp_lower[..]))
                    .unwrap_or(false);
                let on_next_line = lines
                    .get(idx + 1)
                    .map(|nl| nl.to_lowercase().contains(&supp_lower[..]))
                    .unwrap_or(false);
                if on_this_line || on_prev_line || on_next_line {
                    continue;
                }
            }

            // Skip comment lines for structural rules (not secret rules)
            // Secret rules (CWE-798) fire even in comments (leaked credentials)
            if is_comment && rule.cwe != 798 {
                continue;
            }

            // Taint-aware rules: only fire if line is tainted
            let taint_hit = if rule.taint_aware {
                let is_tainted = line_is_tainted(&line_lower, &tainted_vars, &direct_sources, idx);
                if !is_tainted {
                    continue;
                }
                true
            } else {
                false
            };

            let confidence = confidence_for_context(source, line, rule);

            // Skip near-zero confidence findings (avoids noise)
            if confidence < 0.1 {
                continue;
            }

            // Privacy: redact line content for secret-category findings
            // so that actual credentials are never exposed in SAST reports.
            let safe_content = if rule.category == "Hardcoded Secrets" {
                let trimmed = line.trim();
                if let Some(eq_pos) = trimmed.find('=') {
                    // Show key name but redact value: "api_key = [REDACTED]"
                    format!("{}= [REDACTED]", &trimmed[..eq_pos])
                } else if trimmed.len() > 30 {
                    // Find nearest valid UTF-8 boundary at or before byte 20
                    let safe = (0..=20.min(trimmed.len()))
                        .rev()
                        .find(|&i| trimmed.is_char_boundary(i))
                        .unwrap_or(0);
                    format!("{}...[REDACTED]", &trimmed[..safe])
                } else {
                    "[REDACTED — secret detected]".to_string()
                }
            } else {
                line.trim().to_string()
            };

            findings.push(SastFinding {
                rule_id: rule.id.to_string(),
                cwe: rule.cwe,
                severity: rule.severity,
                category: rule.category.to_string(),
                line_number,
                line_content: safe_content,
                confidence,
                description: rule.description.to_string(),
                fix: rule.fix.to_string(),
                taint_flow: taint_hit,
            });
        }
    }

    // Sort by severity descending, then line number
    findings.sort_unstable_by(|a, b| {
        b.severity
            .cmp(&a.severity)
            .then(a.line_number.cmp(&b.line_number))
    });

    let risk_score = compute_risk_score(&findings);

    let critical_count = findings
        .iter()
        .filter(|f| f.severity == Severity::Critical)
        .count();
    let high_count = findings
        .iter()
        .filter(|f| f.severity == Severity::High)
        .count();
    let medium_count = findings
        .iter()
        .filter(|f| f.severity == Severity::Medium)
        .count();
    let low_count = findings
        .iter()
        .filter(|f| f.severity == Severity::Low)
        .count();
    let info_count = findings
        .iter()
        .filter(|f| f.severity == Severity::Info)
        .count();

    let top_fix = findings
        .first()
        .map(|f| format!("[{}] {} — {}", f.rule_id, f.description, f.fix));

    SastReport {
        source: source.to_string(),
        findings,
        risk_score: (risk_score * 100.0).round() / 100.0,
        critical_count,
        high_count,
        medium_count,
        low_count,
        info_count,
        top_fix,
    }
}

// ═══════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════

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

    fn scan(code: &str, file: &str) -> SastReport {
        scan_content(code, file)
    }

    #[test]
    fn test_hardcoded_password_critical() {
        let code = "password = \"hunter2\"";
        let report = scan(code, "auth.py");
        assert!(
            !report.findings.is_empty(),
            "Should flag hardcoded password"
        );
        assert_eq!(report.findings[0].severity, Severity::Critical);
        assert_eq!(report.findings[0].rule_id, "SEC-001");
    }

    #[test]
    fn test_hardcoded_secret_redacted_in_line_content() {
        // Verify that the actual secret value is NEVER exposed in line_content
        let code = "password = \"hunter2\"";
        let report = scan(code, "auth.py");
        let finding = &report.findings[0];
        assert!(
            finding.line_content.contains("[REDACTED]"),
            "Secret-category finding must redact line_content, got: {}",
            finding.line_content
        );
        assert!(
            !finding.line_content.contains("hunter2"),
            "Actual secret value must not appear in line_content: {}",
            finding.line_content
        );
        // Should still show the key name for debugging
        assert!(
            finding.line_content.contains("password"),
            "Key name should be preserved for context: {}",
            finding.line_content
        );
    }

    #[test]
    fn test_openai_key_redacted_not_leaked() {
        let code = r#"client = openai.Client(api_key="sk-proj-abc123xyz")"#;
        let report = scan(code, "llm.py");
        let sec003 = report.findings.iter().find(|f| f.rule_id == "SEC-003");
        assert!(sec003.is_some(), "Should detect sk- prefix");
        let finding = sec003.unwrap();
        assert_eq!(finding.severity, Severity::Critical);
        // Verify actual key is NOT in the finding
        assert!(
            !finding.line_content.contains("sk-proj-abc123xyz"),
            "API key must not appear in SAST finding: {}",
            finding.line_content
        );
        assert!(
            finding.line_content.contains("[REDACTED]"),
            "Finding must contain [REDACTED]: {}",
            finding.line_content
        );
    }

    #[test]
    fn test_non_secret_finding_preserves_line_content() {
        // SQL injection findings should NOT redact — they're not secrets
        let code = r#"cursor.execute("SELECT * FROM users WHERE id=" + user_id)"#;
        let report = scan(code, "db.py");
        let sqli = report
            .findings
            .iter()
            .find(|f| f.category == "SQL Injection");
        if let Some(finding) = sqli {
            assert!(
                !finding.line_content.contains("[REDACTED]"),
                "Non-secret findings should preserve full line content: {}",
                finding.line_content
            );
        }
    }

    #[test]
    fn test_openai_key_flagged() {
        let code = r#"client = openai.Client(api_key="sk-proj-abc123xyz")"#;
        let report = scan(code, "llm.py");
        let sec003 = report.findings.iter().find(|f| f.rule_id == "SEC-003");
        assert!(sec003.is_some(), "Should detect sk- prefix");
        assert_eq!(sec003.unwrap().severity, Severity::Critical);
    }

    #[test]
    fn test_sql_injection_taint_aware() {
        let code = r#"
user_id = request.args.get('id')
query = "SELECT * FROM users WHERE id = %s" % user_id
cursor.execute(query, ())
"#;
        let report = scan(code, "views.py");
        // Should detect the %s pattern in execute — taint-aware
        assert!(!report.findings.is_empty());
    }

    #[test]
    fn test_yaml_load_without_loader() {
        let code = "data = yaml.load(open('config.yml'))";
        let report = scan(code, "config.py");
        let deser = report.findings.iter().find(|f| f.rule_id == "DESER-003");
        assert!(deser.is_some(), "Should flag yaml.load without Loader=");
    }

    #[test]
    fn test_yaml_safe_load_not_flagged() {
        let code = "data = yaml.load(stream, Loader=yaml.SafeLoader)";
        let report = scan(code, "config.py");
        let deser = report.findings.iter().find(|f| f.rule_id == "DESER-003");
        assert!(
            deser.is_none(),
            "yaml.load with Loader= should be suppressed"
        );
    }

    #[test]
    fn test_md5_password_critical() {
        let code = "h = hashlib.md5(password.encode()).hexdigest()";
        let report = scan(code, "utils.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "CRYPTO-004"));
    }

    #[test]
    fn test_os_system_flagged() {
        let code = r#"os.system("rm -rf " + user_path)"#;
        let report = scan(code, "deploy.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "CMD-001"));
        assert_eq!(report.findings[0].severity, Severity::Critical);
    }

    #[test]
    fn test_debug_true_flagged() {
        let code = "DEBUG=True";
        let report = scan(code, "settings.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "AUTH-001"));
    }

    #[test]
    fn test_test_file_lower_confidence() {
        let code = "password = 'test_password_123'";
        let report_prod = scan(code, "auth.py");
        let report_test = scan(code, "test_auth.py");
        // Test file should have lower confidence
        if !report_prod.findings.is_empty() && !report_test.findings.is_empty() {
            assert!(
                report_test.findings[0].confidence < report_prod.findings[0].confidence,
                "Test files should have lower confidence"
            );
        }
    }

    #[test]
    fn test_pickle_loads_critical() {
        let code = r#"
data = request.body
obj = pickle.loads(data)
"#;
        let report = scan(code, "api.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "DESER-001"));
        assert_eq!(
            report
                .findings
                .iter()
                .find(|f| f.rule_id == "DESER-001")
                .unwrap()
                .severity,
            Severity::Critical
        );
    }

    #[test]
    fn test_risk_score_increases_with_severity() {
        let low_code = "h = hashlib.sha1('hello')";
        let crit_code = "pickle.loads(user_data)";
        let low_report = scan(low_code, "utils.py");
        let crit_report = scan(crit_code, "api.py");
        assert!(
            crit_report.risk_score >= low_report.risk_score,
            "Critical finding should produce higher risk score"
        );
    }

    #[test]
    fn test_nosec_suppresses_finding() {
        let code = "api_key = config.get('KEY')  # nosec";
        let report = scan(code, "app.py");
        // The nosec comment should massively reduce confidence, potentially to near-zero
        for f in &report.findings {
            assert!(
                f.confidence < 0.15,
                "nosec should suppress confidence to near-zero"
            );
        }
    }

    #[test]
    fn test_rust_unsafe_without_safety_comment() {
        let code = r#"
fn dangerous() {
    unsafe {
        std::ptr::write(ptr, value);
    }
}
"#;
        let report = scan(code, "memory.rs");
        assert!(report.findings.iter().any(|f| f.rule_id == "MEM-001"));
    }

    #[test]
    fn test_rust_unsafe_with_safety_comment_not_flagged() {
        let code = r#"
fn read_aligned(ptr: *const u8) -> u8 {
    // SAFETY: ptr is guaranteed aligned and valid by the caller contract
    unsafe { *ptr }
}
"#;
        let report = scan(code, "memory.rs");
        // MEM-001 should not fire because "// safety:" is present
        assert!(
            !report.findings.iter().any(|f| f.rule_id == "MEM-001"),
            "Unsafe with SAFETY: comment should not be flagged"
        );
    }

    #[test]
    fn test_xss_innerhtml_flagged() {
        let code = r#"element.innerHTML = userInput;"#;
        let report = scan(code, "ui.js");
        assert!(report.findings.iter().any(|f| f.rule_id == "XSS-001"));
    }

    #[test]
    fn test_jwt_algorithms_none() {
        let code = r#"payload = jwt.decode(token, algorithms=["none"])"#;
        let report = scan(code, "auth.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "AUTH-007"));
        assert_eq!(
            report
                .findings
                .iter()
                .find(|f| f.rule_id == "AUTH-007")
                .unwrap()
                .severity,
            Severity::Critical
        );
    }

    #[test]
    fn test_empty_file_zero_risk() {
        let report = scan("", "empty.py");
        assert!(report.findings.is_empty());
        assert_eq!(report.risk_score, 0.0);
    }

    #[test]
    fn test_taint_propagation() {
        let code = r#"
raw = request.args.get('cmd')
sanitized = raw.strip()
result = os.system(sanitized)
"#;
        // os.system is already flagged unconditionally (CMD-001)
        let report = scan(code, "run.py");
        assert!(report.findings.iter().any(|f| f.rule_id == "CMD-001"));
    }

    #[test]
    fn test_path_traversal_not_flagged_in_markdown() {
        // Markdown relative links are NOT security vulnerabilities
        let code = "See the [setup guide](../docs/getting-started.md) for details.";
        let report = scan(code, "README.md");
        let path_findings: Vec<_> = report
            .findings
            .iter()
            .filter(|f| f.rule_id == "PATH-002")
            .collect();
        assert!(
            path_findings.is_empty(),
            "PATH-002 should not fire on markdown files (found {} findings)",
            path_findings.len()
        );
    }

    #[test]
    fn test_path_traversal_still_flagged_in_python() {
        // Python code with ../ IS suspicious
        let code = r#"path = os.path.join(base, "../../../etc/passwd")"#;
        let report = scan(code, "handler.py");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "PATH-002"),
            "PATH-002 should still fire on Python files"
        );
    }

    #[test]
    fn test_hardcoded_secret_still_flagged_in_markdown() {
        // Secrets in docs ARE real findings — someone pasted credentials
        let code = "Example: password = \"hunter2\"";
        let report = scan(code, "setup.md");
        assert!(
            report.findings.iter().any(|f| f.cwe == 798),
            "Hardcoded secrets should still be flagged in markdown files"
        );
    }

    // ── P0: C/C++ SAST ──────────────────────────────────────────────

    #[test]
    fn test_cpp_buffer_overflow_strcpy() {
        let code = r#"void copy_name(char *dst, const char *src) { strcpy(dst, src); }"#;
        let report = scan(code, "util.c");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CPP-001"),
            "Should detect strcpy buffer overflow"
        );
    }

    #[test]
    fn test_cpp_gets() {
        let code = r#"void read_input() { char buf[64]; gets(buf); }"#;
        let report = scan(code, "input.c");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CPP-002"),
            "Should detect gets()"
        );
    }

    #[test]
    fn test_cpp_system_injection() {
        // Multi-line with taint source: argv flows into system()
        let code =
            "int main(int argc, char* argv[]) {\n    char* cmd = argv[1];\n    system(cmd);\n}";
        let report = scan(code, "exec.cpp");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CPP-008"),
            "Should detect system() command injection, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_cpp_format_string() {
        // printf with argv on the same line — direct taint source + sink
        let code = "int main(int argc, char* argv[]) {\n    printf(argv[1]);\n}";
        let report = scan(code, "fmt.c");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CPP-006"),
            "Should detect printf format string with argv, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_c_inherits_cpp_rules() {
        // .c files should match CPP-* rules via rule_applies inheritance
        let code = r#"void copy(char *d) { strcpy(d, "hello"); }"#;
        let report = scan(code, "copy.c");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CPP-001"),
            ".c files should match CPP rules"
        );
    }

    // ── P0: Swift SAST ──────────────────────────────────────────────

    #[test]
    fn test_swift_userdefaults_password() {
        let code = r#"UserDefaults.standard.set(password, forKey: "password")"#;
        let report = scan(code, "auth.swift");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "SWIFT-003"),
            "Should detect password storage in UserDefaults"
        );
    }

    #[test]
    fn test_swift_nscoding_deserialization() {
        let code = r#"let obj = NSKeyedUnarchiver.unarchiveObject(with: data)"#;
        let report = scan(code, "decode.swift");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "SWIFT-005"),
            "Should detect NSKeyedUnarchiver"
        );
    }

    // ── P1: C# SAST ────────────────────────────────────────────────

    #[test]
    fn test_cs_binaryformatter() {
        let code = r#"var obj = new BinaryFormatter().Deserialize(stream);"#;
        let report = scan(code, "loader.cs");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CS-002"),
            "Should detect BinaryFormatter deserialization"
        );
    }

    #[test]
    fn test_cs_process_start() {
        // Taint: Request.Query flows into Process.Start
        let code =
            "var userInput = Request.Query[\"cmd\"];\nProcess.Start(\"cmd.exe\", userInput);";
        let report = scan(code, "run.cs");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CS-003"),
            "Should detect Process.Start injection, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_cs_sql_injection() {
        // Taint: Request.Form flows into SqlCommand
        let code = "var userId = Request.Form[\"id\"];\nvar cmd = new SqlCommand(\"SELECT * FROM users WHERE id=\" + userId);";
        let report = scan(code, "data.cs");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CS-004"),
            "Should detect SqlCommand SQL injection, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    // ── P2: PHP SAST ────────────────────────────────────────────────

    #[test]
    fn test_php_eval() {
        let code = r#"eval($code);"#;
        let report = scan(code, "handler.php");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "PHP-005"),
            "Should detect PHP eval()"
        );
    }

    #[test]
    fn test_php_sql_injection() {
        // Taint: $_GET superglobal is a direct taint source
        let code = "$id = $_GET['id'];\n$result = mysql_query(\"SELECT * FROM users WHERE id=\" . $_GET['id']);";
        let report = scan(code, "query.php");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "PHP-001"),
            "Should detect mysql_query SQL injection, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_php_xss() {
        // Taint: $_GET is a direct taint source on the echo line
        let code = "$name = $_GET['name'];\necho $_GET['name'];";
        let report = scan(code, "view.php");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "PHP-008"),
            "Should detect reflected XSS via echo, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_php_unserialize() {
        let code = r#"$obj = unserialize($data);"#;
        let report = scan(code, "cache.php");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "PHP-009"),
            "Should detect insecure unserialize"
        );
    }

    // ── Frontend Framework SAST ─────────────────────────────────────

    #[test]
    fn test_vue_v_html() {
        let code = r#"<div v-html="userContent"></div>"#;
        let report = scan(code, "component.vue");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "VUE-001"),
            "Should detect v-html XSS, findings: {:?}",
            report
                .findings
                .iter()
                .map(|f| &f.rule_id)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_angular_bypass_security() {
        let code = r#"this.sanitizer.bypassSecurityTrustHtml(userInput);"#;
        let report = scan(code, "component.ts");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "NG-001"),
            "Should detect Angular DomSanitizer bypass"
        );
    }

    #[test]
    fn test_angular_innerhtml_binding() {
        let code = r#"<div [innerHTML]="rawHtml"></div>"#;
        let report = scan(code, "template.html");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "NG-002"),
            "Should detect Angular [innerHTML] binding"
        );
    }

    #[test]
    fn test_svelte_html_tag() {
        let code = r#"{@html content}"#;
        let report = scan(code, "page.svelte");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "SVELTE-001"),
            "Should detect Svelte {{@html}} XSS"
        );
    }

    #[test]
    fn test_html_javascript_uri() {
        let code = r#"<a href="javascript:alert(1)">click</a>"#;
        let report = scan(code, "page.html");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "HTML-003"),
            "Should detect javascript: URI"
        );
    }

    #[test]
    fn test_css_expression() {
        let code = r#"div { width: expression(document.body.clientWidth); }"#;
        let report = scan(code, "styles.css");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CSS-001"),
            "Should detect CSS expression()"
        );
    }

    #[test]
    fn test_css_javascript_url() {
        let code = r#"div { background: url(javascript:alert(1)); }"#;
        let report = scan(code, "styles.css");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "CSS-002"),
            "Should detect CSS url(javascript:)"
        );
    }

    #[test]
    fn test_localstorage_token() {
        let code = r#"localStorage.setItem('token', authResponse.token);"#;
        let report = scan(code, "auth.ts");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "FE-002"),
            "Should detect token in localStorage"
        );
    }

    #[test]
    fn test_postmessage_star_origin() {
        let code = r#"window.postMessage(sensitiveData, '*');"#;
        let report = scan(code, "iframe.ts");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "FE-004"),
            "Should detect postMessage with * origin"
        );
    }

    #[test]
    fn test_target_blank_without_noopener() {
        let code = r#"<a href="https://evil.com" target="_blank">Link</a>"#;
        let report = scan(code, "nav.html");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "HTML-004"),
            "Should detect target=_blank without noopener"
        );
    }

    #[test]
    fn test_target_blank_with_noopener_suppressed() {
        let code =
            r#"<a href="https://safe.com" target="_blank" rel="noopener noreferrer">Link</a>"#;
        let report = scan(code, "nav.html");
        assert!(
            !report.findings.iter().any(|f| f.rule_id == "HTML-004"),
            "Should NOT flag target=_blank when noopener is present"
        );
    }

    #[test]
    fn test_vue_file_detects_js_rules() {
        // Vue files should also match JS/TS rules via inheritance
        let code = r#"document.getElementById("app").innerHTML = data"#;
        let report = scan(code, "handler.vue");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "XSS-001"),
            "Vue files should inherit JS XSS rules"
        );
    }

    #[test]
    fn test_html_onerror_xss() {
        let code = r#"<img src="x" onerror="alert(1)">"#;
        let report = scan(code, "page.html");
        assert!(
            report.findings.iter().any(|f| f.rule_id == "HTML-001"),
            "Should detect inline onerror handler"
        );
    }
}
