/**
 * Dependency Vulnerability Scanner
 *
 * Scans package dependencies for known vulnerabilities using npm audit.
 * Falls back to parsing package-lock.json if npm audit is not available.
 *
 * @module scanners/dependencies
 */
import spawn from "cross-spawn";
import { access } from "fs/promises";
import { join } from "path";
import { SEVERITY_MAPPINGS } from "./types.js";
import { logger } from "../logger.js";
/**
 * Run npm audit and return findings
 */
export async function runDependencyAudit(projectPath) {
    const startTime = Date.now();
    const findings = [];
    // Check if package.json exists
    const packageJsonPath = join(projectPath, "package.json");
    try {
        await access(packageJsonPath);
    }
    catch {
        return {
            scanner: "npm-audit",
            findings: [],
            duration: Date.now() - startTime,
            success: true,
            error: "No package.json found - skipping dependency audit",
        };
    }
    try {
        const result = await runNpmAudit(projectPath);
        if (result.error) {
            logger.warn("scanners.npm_audit_error", { error: result.error });
            return {
                scanner: "npm-audit",
                findings: [],
                duration: Date.now() - startTime,
                success: false,
                error: result.error,
                exitCode: result.exitCode,
            };
        }
        if (result.output) {
            const auditOutput = JSON.parse(result.output);
            const parsed = parseNpmAuditOutput(auditOutput, projectPath);
            findings.push(...parsed);
        }
        logger.info("scanners.npm_audit_complete", {
            projectPath,
            findingsCount: findings.length,
            duration: Date.now() - startTime,
        });
        return {
            scanner: "npm-audit",
            findings,
            duration: Date.now() - startTime,
            success: true,
            filesScanned: 1, // package-lock.json
        };
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logger.error("scanners.npm_audit_failed", { error: errorMessage });
        return {
            scanner: "npm-audit",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: errorMessage,
        };
    }
}
/**
 * Execute npm audit command
 */
async function runNpmAudit(projectPath) {
    return new Promise((resolve) => {
        const child = spawn("npm", ["audit", "--json"], {
            cwd: projectPath,
            timeout: 60000, // 1 minute timeout
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            // npm audit returns non-zero if vulnerabilities found, which is expected
            if (stdout) {
                resolve({ output: stdout, exitCode: code ?? undefined });
            }
            else if (stderr && !stdout) {
                resolve({ error: stderr, exitCode: code ?? undefined });
            }
            else {
                resolve({ output: stdout || "{}", exitCode: code ?? undefined });
            }
        });
        child.on("error", (err) => {
            resolve({ error: err.message });
        });
    });
}
/**
 * Parse npm audit JSON output into DeterministicFindings
 */
function parseNpmAuditOutput(audit, projectPath) {
    const findings = [];
    if (!audit.vulnerabilities) {
        return findings;
    }
    for (const [pkgName, vuln] of Object.entries(audit.vulnerabilities)) {
        // Get CWE and CVE from via objects
        const cweIds = [];
        const cveIds = [];
        let title = `Vulnerable dependency: ${pkgName}`;
        let url = "";
        for (const via of vuln.via) {
            if (typeof via === "object") {
                if (via.cwe) {
                    cweIds.push(...via.cwe);
                }
                if (via.title) {
                    title = via.title;
                }
                if (via.url) {
                    url = via.url;
                    // Extract CVE from URL if present
                    const cveMatch = via.url.match(/CVE-\d{4}-\d+/);
                    if (cveMatch) {
                        cveIds.push(cveMatch[0]);
                    }
                }
            }
        }
        // Map npm severity to vaspera severity
        const severity = SEVERITY_MAPPINGS.npm[vuln.severity] || "medium";
        // Determine if fix is available
        let fixAvailable = false;
        let fix;
        if (typeof vuln.fixAvailable === "boolean") {
            fixAvailable = vuln.fixAvailable;
        }
        else if (vuln.fixAvailable && typeof vuln.fixAvailable === "object") {
            fixAvailable = true;
            fix = `Update to ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`;
        }
        findings.push({
            scanner: "npm-audit",
            ruleId: `npm-audit:${pkgName}`,
            file: "package.json",
            line: 1, // npm audit doesn't provide line numbers
            message: `${title} in ${pkgName}@${vuln.range}`,
            severity,
            confidence: 100,
            cweIds: cweIds.length > 0 ? [...new Set(cweIds)] : undefined,
            cveIds: cveIds.length > 0 ? [...new Set(cveIds)] : undefined,
            fixAvailable,
            fix,
            evidence: `Package: ${pkgName}\nRange: ${vuln.range}\nDirect: ${vuln.isDirect}\n${url ? `URL: ${url}` : ""}`,
            metadata: {
                packageName: pkgName,
                isDirect: vuln.isDirect,
                effects: vuln.effects,
                nodes: vuln.nodes,
            },
        });
    }
    return findings;
}
/**
 * Check if npm is available
 */
export async function checkNpmAvailable() {
    return new Promise((resolve) => {
        const child = spawn("npm", ["--version"]);
        let version = "";
        child.stdout?.on("data", (data) => {
            version += data.toString().trim();
        });
        child.on("close", (code) => {
            if (code === 0 && version) {
                resolve({ available: true, version });
            }
            else {
                resolve({ available: false, error: "npm not found" });
            }
        });
        child.on("error", () => {
            resolve({ available: false, error: "npm not found" });
        });
    });
}
//# sourceMappingURL=dependencies.js.map