/**
 * Semgrep Scanner
 *
 * Runs Semgrep with OWASP, JavaScript/TypeScript, and custom rules.
 * Outputs SARIF format which we parse into DeterministicFindings.
 *
 * If Semgrep is not available, returns empty results with instruction
 * to install: `pip install semgrep` or `brew install semgrep`
 *
 * @module scanners/semgrep
 */
import spawn from "cross-spawn";
import { access, mkdir, writeFile } from "fs/promises";
import { join, relative } from "path";
import { logger } from "../logger.js";
/**
 * Default Semgrep rule configs to use
 */
const DEFAULT_CONFIGS = [
    "p/owasp-top-ten",
    "p/javascript",
    "p/typescript",
    "p/react",
    "p/nodejs",
];
/**
 * Optional configs that may not be available
 */
const OPTIONAL_CONFIGS = [
    "p/supabase",
    "p/nextjs",
];
/**
 * Run Semgrep analysis and return findings
 */
export async function runSemgrep(projectPath, options) {
    const startTime = Date.now();
    const findings = [];
    // Check if Semgrep is available
    const semgrepAvailable = await checkSemgrepAvailable();
    if (!semgrepAvailable.available) {
        logger.warn("scanners.semgrep_not_available", {
            error: semgrepAvailable.error,
            install: "pip install semgrep",
        });
        return {
            scanner: "semgrep",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: `Semgrep not available: ${semgrepAvailable.error}. Install with: pip install semgrep`,
        };
    }
    logger.info("scanners.semgrep_starting", {
        version: semgrepAvailable.version,
        configs: options?.configs || DEFAULT_CONFIGS,
    });
    try {
        // Build config arguments
        const configs = options?.configs || DEFAULT_CONFIGS;
        const configArgs = [];
        for (const config of configs) {
            configArgs.push("--config", config);
        }
        // Add custom rules directory if provided
        if (options?.customRulesDir) {
            try {
                await access(options.customRulesDir);
                configArgs.push("--config", options.customRulesDir);
            }
            catch {
                // Custom rules dir doesn't exist, skip
            }
        }
        // Check for project-local rules
        const localRulesDir = join(projectPath, ".vaspera", "semgrep-rules");
        try {
            await access(localRulesDir);
            configArgs.push("--config", localRulesDir);
        }
        catch {
            // No local rules
        }
        // Run Semgrep
        const result = await runSemgrepCli(projectPath, configArgs, options?.timeout);
        if (result.error && !result.output) {
            return {
                scanner: "semgrep",
                findings: [],
                duration: Date.now() - startTime,
                success: false,
                error: result.error,
                exitCode: result.exitCode,
            };
        }
        if (result.output) {
            const sarif = JSON.parse(result.output);
            const parsed = parseSarifOutput(sarif, projectPath);
            findings.push(...parsed);
        }
        logger.info("scanners.semgrep_complete", {
            findingsCount: findings.length,
            duration: Date.now() - startTime,
        });
        return {
            scanner: "semgrep",
            findings,
            duration: Date.now() - startTime,
            success: true,
            version: semgrepAvailable.version,
            rulesUsed: configs,
        };
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logger.error("scanners.semgrep_failed", { error: errorMessage });
        return {
            scanner: "semgrep",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: errorMessage,
        };
    }
}
/**
 * Execute Semgrep CLI
 */
async function runSemgrepCli(projectPath, configArgs, timeout) {
    return new Promise((resolve) => {
        const args = [
            ...configArgs,
            "--sarif",
            "--quiet",
            "--no-git-ignore", // Respect .gitignore but don't require git
            projectPath,
        ];
        const child = spawn("semgrep", args, {
            cwd: projectPath,
            timeout: timeout || 300000, // 5 minute default timeout
            env: {
                ...process.env,
                SEMGREP_SEND_METRICS: "off", // Don't send telemetry
            },
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            // Semgrep returns 1 if findings, 0 if clean
            if (stdout) {
                resolve({ output: stdout, exitCode: code ?? undefined });
            }
            else if (code !== 0 && stderr) {
                resolve({ error: stderr, exitCode: code ?? undefined });
            }
            else {
                // No output, no errors - return empty SARIF
                resolve({
                    output: JSON.stringify({ $schema: "", version: "2.1.0", runs: [] }),
                    exitCode: code ?? undefined,
                });
            }
        });
        child.on("error", (err) => {
            resolve({ error: err.message });
        });
    });
}
/**
 * Parse SARIF output into DeterministicFindings
 */
function parseSarifOutput(sarif, projectPath) {
    const findings = [];
    for (const run of sarif.runs) {
        const ruleMap = new Map();
        for (const rule of run.tool.driver.rules) {
            ruleMap.set(rule.id, rule);
        }
        for (const result of run.results) {
            const rule = ruleMap.get(result.ruleId);
            const location = result.locations?.[0]?.physicalLocation;
            if (!location)
                continue;
            const filePath = location.artifactLocation.uri;
            const relativePath = filePath.startsWith("file://")
                ? relative(projectPath, filePath.slice(7))
                : relative(projectPath, filePath);
            // Map Semgrep severity to vaspera severity
            const severity = mapSemgrepSeverity(result.level, rule);
            // Extract CWE IDs
            const cweIds = rule?.properties?.cwe || [];
            findings.push({
                scanner: "semgrep",
                ruleId: `semgrep:${result.ruleId}`,
                file: relativePath,
                line: location.region.startLine,
                column: location.region.startColumn,
                endLine: location.region.endLine,
                endColumn: location.region.endColumn,
                message: result.message.text,
                severity,
                confidence: 100,
                cweIds: cweIds.length > 0 ? cweIds : undefined,
                evidence: location.region.snippet?.text,
                metadata: {
                    ruleName: rule?.name,
                    help: rule?.help?.text,
                    tags: rule?.properties?.tags,
                    precision: rule?.properties?.precision,
                },
            });
        }
    }
    return findings;
}
/**
 * Map Semgrep/SARIF severity levels to vaspera severity
 */
function mapSemgrepSeverity(level, rule) {
    // Check for security-severity score
    const securitySeverity = rule?.properties?.["security-severity"];
    if (securitySeverity) {
        const score = parseFloat(securitySeverity);
        if (score >= 9.0)
            return "critical";
        if (score >= 7.0)
            return "high";
        if (score >= 4.0)
            return "medium";
        if (score >= 0.1)
            return "low";
        return "info";
    }
    // Map SARIF levels
    switch (level) {
        case "error":
            return "high";
        case "warning":
            return "medium";
        case "note":
            return "low";
        case "none":
        default:
            return "info";
    }
}
/**
 * Check if Semgrep is available
 */
export async function checkSemgrepAvailable() {
    return new Promise((resolve) => {
        const child = spawn("semgrep", ["--version"], {
            timeout: 10000,
        });
        let version = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            version += data.toString().trim();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString().trim();
        });
        child.on("close", (code) => {
            if (code === 0 && version) {
                resolve({ available: true, version });
            }
            else {
                resolve({
                    available: false,
                    error: stderr || "semgrep not found",
                });
            }
        });
        child.on("error", () => {
            resolve({
                available: false,
                error: "semgrep not found - install with: pip install semgrep",
            });
        });
    });
}
/**
 * Generate a sample Semgrep rules file for Supabase RLS
 */
export async function generateSupabaseRules(outputDir) {
    const rulesYaml = `# Supabase RLS Rules for Vaspera Hardening
# Place this file in .vaspera/semgrep-rules/

rules:
  - id: supabase-rls-missing
    patterns:
      - pattern: |
          createTable($TABLE, ...)
      - pattern-not-inside: |
          enableRls(...)
    message: "Table created without Row Level Security enabled"
    languages: [typescript, javascript]
    severity: ERROR
    metadata:
      cwe:
        - "CWE-284"
      category: security
      technology:
        - supabase

  - id: supabase-service-role-key
    pattern-either:
      - pattern: |
          createClient($URL, process.env.SUPABASE_SERVICE_ROLE_KEY, ...)
      - pattern: |
          createClient($URL, $SERVICE_KEY, ...)
    message: "Using service role key bypasses RLS - ensure this is intentional"
    languages: [typescript, javascript]
    severity: WARNING
    metadata:
      category: security
      technology:
        - supabase

  - id: supabase-no-auth-check
    patterns:
      - pattern: |
          supabase.from($TABLE).select(...)
      - pattern-not-inside: |
          if ($AUTH) { ... }
      - pattern-not-inside: |
          $AUTH && ...
    message: "Database query without authentication check"
    languages: [typescript, javascript]
    severity: WARNING
    metadata:
      cwe:
        - "CWE-862"
      category: security
      technology:
        - supabase
`;
    const rulesDir = join(outputDir, ".vaspera", "semgrep-rules");
    await mkdir(rulesDir, { recursive: true });
    const rulesPath = join(rulesDir, "supabase.yaml");
    await writeFile(rulesPath, rulesYaml, "utf-8");
    return rulesPath;
}
//# sourceMappingURL=semgrep.js.map