/**
 * CVE Checker with transitive dependency scanning and triage capability.
 *
 * Fix 9: Distinguishes exploitable (direct dependency) CVEs from transitive CVEs.
 * Fix (new): Full transitive dependency tree scanning via npm ls --json.
 */
import { execSync } from 'child_process';
import { resolve } from 'path';
import { LRUCache } from 'lru-cache';
import { OsvClient } from '../clients/osv-client.js';
import { NvdClient } from '../clients/nvd-client.js';
import { StructuredLogger } from '../utils/structured-logger.js';
import { readCveDiskCache } from '../utils/cve-api-disk-cache.js';
import { dedupeCveFindings } from '../utils/cve-canonical.js';
// GAP 9: LRU cache for CVE results (500 entries, 15 min TTL)
const cveCache = new LRUCache({
    max: 500,
    ttl: 15 * 60 * 1000,
    updateAgeOnGet: false,
});
export class CveChecker {
    osv;
    nvd;
    constructor() {
        this.osv = new OsvClient();
        this.nvd = new NvdClient();
    }
    async check(packageName, version) {
        const cacheKey = `${packageName}@${version ?? 'latest'}`;
        const cached = cveCache.get(cacheKey);
        if (cached)
            return { findings: cached, lookupStatus: 'ok' };
        const osv = await this.osv.check(packageName, version);
        if (osv.stale || osv.status !== 'ok') {
            this.emitDegraded(packageName, 'osv', osv.status, osv.stale ?? false);
        }
        const nvd = await this.nvd.search(packageName);
        if (nvd.stale || nvd.status !== 'ok') {
            this.emitDegraded(packageName, 'nvd', nvd.status, nvd.stale ?? false);
        }
        const staleDisk = this.staleDiskFindings(packageName, version);
        const findings = dedupeCveFindings([...osv.findings, ...nvd.findings, ...staleDisk]);
        cveCache.set(cacheKey, findings);
        const lookupStatus = osv.status === 'unavailable' && nvd.status === 'unavailable'
            ? 'unavailable'
            : osv.status === 'degraded' || nvd.status === 'degraded' || osv.stale || nvd.stale
                ? 'degraded'
                : 'ok';
        return {
            findings,
            lookupStatus: findings.length > 0 && lookupStatus === 'degraded' ? 'degraded' : lookupStatus,
        };
    }
    staleDiskFindings(packageName, version) {
        const osvDisk = readCveDiskCache(`osv:${packageName}@${version ?? 'latest'}`);
        if (osvDisk?.data?.findings?.length)
            return osvDisk.data.findings;
        const nvdDisk = readCveDiskCache(`nvd:${packageName}`);
        if (nvdDisk?.data?.findings?.length)
            return nvdDisk.data.findings;
        return [];
    }
    emitDegraded(packageName, source, status, stale) {
        StructuredLogger.warn({
            event: 'cve_lookup_degraded',
            packageName,
            source,
            status,
            stale,
            message: stale
                ? `CVE lookup for ${packageName} using stale ${source} cache`
                : `CVE lookup for ${packageName} degraded (${source}: ${status})`,
        });
    }
    /** Scan server name plus packages discovered in command/args */
    async checkServerPackages(serverName, packages, version) {
        const targets = packages.length > 0 ? packages : [serverName];
        const all = [];
        let worst = 'ok';
        for (const pkg of targets) {
            const result = await this.check(pkg, version);
            if (result.lookupStatus === 'unavailable')
                worst = 'unavailable';
            else if (result.lookupStatus === 'degraded' && worst === 'ok')
                worst = 'degraded';
            all.push(...result.findings);
        }
        return { findings: dedupeCveFindings(all), lookupStatus: worst };
    }
    async checkWithTriage(packageName, directDeps = [], version) {
        const { findings: all } = await this.check(packageName, version);
        if (directDeps.length === 0)
            return { all, direct: all, transitive: [] };
        const directSet = new Set(directDeps.map(d => d.toLowerCase()));
        const direct = [];
        const transitive = [];
        for (const cve of all) {
            const cvePkg = cve.id.toLowerCase().replace(/[^a-z0-9\-]/g, '');
            const cveSummary = cve.summary.toLowerCase();
            let isDirect = false;
            for (const dep of directSet) {
                if (cvePkg.includes(dep) || dep.includes(cvePkg) || cveSummary.includes(dep)) {
                    isDirect = true;
                    break;
                }
            }
            if (isDirect)
                direct.push(cve);
            else
                transitive.push(cve);
        }
        return { all, direct, transitive };
    }
    /**
     * Recursively collect the full dependency tree from npm ls --json.
     * Returns a unique set of name@version pairs.
     */
    static getTransitiveDependencies(packagePath) {
        const cwd = resolve(packagePath);
        const deps = new Map();
        // Try reading package-lock.json first (faster, no npm dependency)
        const lockPath = resolve(cwd, 'package-lock.json');
        const pnpmLockPath = resolve(cwd, 'pnpm-lock.yaml');
        const yarnLockPath = resolve(cwd, 'yarn.lock');
        // Fall back to npm ls for universal support
        try {
            const output = execSync('npm ls --json --all --depth=Infinity', {
                cwd,
                encoding: 'utf-8',
                maxBuffer: 50 * 1024 * 1024,
                timeout: 30000,
            });
            const tree = JSON.parse(output);
            const walk = (node) => {
                if (node.dependencies) {
                    for (const [name, info] of Object.entries(node.dependencies)) {
                        if (info.version && !deps.has(name)) {
                            deps.set(name, info.version);
                        }
                        walk(info);
                    }
                }
            };
            walk(tree);
        }
        catch {
            // npm ls failed — return empty, caller should handle gracefully
        }
        return deps;
    }
    /**
     * Full scan: collect all transitive dependencies, then check each for CVEs.
     * This is the production-grade scanning path that closes the major gap.
     */
    async scanAllDependencies(packagePath) {
        const transitiveDeps = CveChecker.getTransitiveDependencies(packagePath);
        const allResults = [];
        const seen = new Set();
        // Limit to prevent hammering OSV/NVD APIs — cap at 200 packages
        const entries = Array.from(transitiveDeps.entries()).slice(0, 200);
        for (const [pkgName, version] of entries) {
            if (seen.has(pkgName))
                continue;
            seen.add(pkgName);
            try {
                const { findings: cves } = await this.check(pkgName, version);
                allResults.push(...cves);
            }
            catch {
                // Individual package check failure shouldn't abort the whole scan
            }
        }
        return allResults;
    }
}
//# sourceMappingURL=cve-checker.js.map