// dep-auditor/index.ts — Main dependency audit orchestrator (SPEC-025, SPEC-335)
// Combines license checking, vuln detection, abandonment heuristics, and duplicate detection.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { readManifest } from '../stack-auditor/manifest-reader.js';
import { checkLicense, isCommercialProject } from './license-checker.js';
import { detectDuplicates } from './dep-duplicates-detector.js';
import { getVulns, KNOWN_VULNS } from './vuln-data.js';
import { getAbandonedInfo, classifyEntry } from './abandonment-data.js';
import { fetchNpmCve } from './cve-fetcher.js';
import { parseTransitiveDeps } from './lockfile-parser.js';
export { formatDepAuditMarkdown, formatDepAuditAcceptanceCriteria } from './formatters.js';
// ---------------------------------------------------------------------------
// 1h in-memory CVE cache
// ---------------------------------------------------------------------------
const cveCache = new Map();
const CVE_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
/** Clear the CVE cache — intended for use in tests only. */
export function clearCveCache() {
    cveCache.clear();
}
async function fetchNpmCveCached(pkg, version) {
    const cacheKey = `${pkg}@${version}`;
    const now = Date.now();
    const cached = cveCache.get(cacheKey);
    if (cached && cached.expiresAt > now) {
        return cached.data;
    }
    const data = await fetchNpmCve(pkg, version);
    cveCache.set(cacheKey, { data, expiresAt: now + CVE_CACHE_TTL_MS });
    return data;
}
/** Build DepVuln entries from live CveFinding results, marking as transitive if needed. */
function cveToDepVulns(findings, transitive) {
    return findings.map((f) => ({
        cveId: f.cveId,
        severity: f.severity,
        description: f.title,
        fixedIn: f.fixedIn ?? undefined,
        ...(transitive ? { transitive: true } : {}),
    }));
}
/**
 * Merge hardcoded vulns with live API vulns. Live results take precedence:
 * if a CVE ID appears in both, keep the live version.
 */
function mergeVulns(hardcoded, live) {
    const liveCveIds = new Set(live.map((v) => v.cveId));
    const filteredHardcoded = hardcoded.filter((v) => !liveCveIds.has(v.cveId));
    return [...filteredHardcoded, ...live];
}
/** Build a DepAuditEntry for a single direct dependency. */
async function buildDirectEntry(name, version, ecosystem, licenseMap) {
    const hardcodedVulns = getVulns(name, version);
    let liveVulns = [];
    if (ecosystem === 'nodejs' && name in KNOWN_VULNS) {
        try {
            const findings = await fetchNpmCveCached(name, version);
            liveVulns = cveToDepVulns(findings, false);
        }
        catch {
            // silently fall back to hardcoded list
        }
    }
    const vulns = mergeVulns(hardcodedVulns, liveVulns);
    const licenseRaw = licenseMap[name] ?? '';
    const license = checkLicense(licenseRaw);
    const abandoned = getAbandonedInfo(name);
    return { name, currentVersion: version, ecosystem, vulns, license, abandoned };
}
/** Build transitive-only vuln entries (packages not in direct deps). */
async function buildTransitiveEntries(transitiveOnlyEntries, ecosystem) {
    const results = await Promise.all(transitiveOnlyEntries.map(async ([name, version]) => {
        const hardcoded = getVulns(name, version).map((v) => ({ ...v, transitive: true }));
        let liveVulns = [];
        if (name in KNOWN_VULNS) {
            try {
                liveVulns = cveToDepVulns(await fetchNpmCveCached(name, version), true);
            }
            catch {
                /* fall back to hardcoded */
            }
        }
        const vulns = mergeVulns(hardcoded, liveVulns);
        if (vulns.length === 0) {
            return null;
        }
        return {
            name,
            currentVersion: version,
            ecosystem,
            vulns,
            license: checkLicense(''),
            abandoned: getAbandonedInfo(name),
        };
    }));
    return results.filter((e) => e !== null);
}
/** Classify tagged entries into critical / warnings / clean buckets. */
function classifyEntries(taggedEntries) {
    const critical = [];
    const warnings = [];
    const clean = [];
    for (const entry of taggedEntries) {
        const cls = classifyEntry(entry);
        if (cls === 'critical') {
            critical.push(entry);
        }
        else if (cls === 'warning') {
            warnings.push(entry);
        }
        else {
            clean.push(entry);
        }
    }
    return { critical, warnings, clean };
}
/**
 * Audit all dependencies of a project at projectPath.
 * Scans package.json (npm), requirements.txt (python), go.mod (go),
 * Cargo.toml (rust), pom.xml (java).
 */
export async function auditDeps(projectPath) {
    const manifest = await readManifest(projectPath);
    const ecosystem = manifest.ecosystem;
    if (ecosystem === 'nodejs') {
        try {
            isCommercialProject(await readFile(join(projectPath, 'package.json'), 'utf-8'));
        }
        catch {
            /* default: assume commercial */
        }
    }
    const deps = manifest.directDependencies;
    const depNames = Object.keys(deps);
    const licenseMap = await buildLicenseMap(projectPath, ecosystem);
    const entries = await Promise.all(depNames.map((name) => buildDirectEntry(name, deps[name] ?? 'unknown', ecosystem, licenseMap)));
    const transitiveOnlyEntries = ecosystem === 'nodejs'
        ? [...(await parseTransitiveDeps(projectPath)).entries()].filter(([n]) => !new Set(depNames).has(n))
        : [];
    const transitiveVulnEntries = await buildTransitiveEntries(transitiveOnlyEntries, ecosystem);
    const duplicates = detectDuplicates(depNames);
    const dupCategories = new Map(duplicates.flatMap((d) => d.packages.map((p) => [p, d.category])));
    const tagged = [...entries, ...transitiveVulnEntries].map((e) => {
        const grp = dupCategories.get(e.name);
        return grp ? { ...e, duplicateGroup: grp } : e;
    });
    const { critical, warnings, clean } = classifyEntries(tagged);
    const summary = buildSummary(depNames.length, critical.length, warnings.length, duplicates.length);
    return { ecosystem, totalDeps: depNames.length, critical, warnings, clean, duplicates, summary };
}
async function buildLicenseMap(projectPath, ecosystem) {
    if (ecosystem !== 'nodejs') {
        return {};
    }
    const licenseMap = {};
    try {
        const pkgJson = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const parsed = JSON.parse(pkgJson);
        const allDeps = {
            ...(parsed.dependencies ?? {}),
            ...(parsed.devDependencies ?? {}),
        };
        for (const depName of Object.keys(allDeps)) {
            try {
                const depPkg = await readFile(join(projectPath, 'node_modules', depName, 'package.json'), 'utf-8');
                const depParsed = JSON.parse(depPkg);
                if (typeof depParsed.license === 'string') {
                    licenseMap[depName] = depParsed.license;
                }
            }
            catch {
                // dep not installed or not accessible — leave as unknown
            }
        }
    }
    catch {
        // no package.json
    }
    return licenseMap;
}
function buildSummary(total, criticalCount, warningCount, duplicateCount) {
    const parts = [`${total} dep(s) scanned`];
    if (criticalCount > 0) {
        parts.push(`${criticalCount} critical issue(s)`);
    }
    if (warningCount > 0) {
        parts.push(`${warningCount} warning(s)`);
    }
    if (duplicateCount > 0) {
        parts.push(`${duplicateCount} duplicate group(s) detected`);
    }
    if (criticalCount === 0 && warningCount === 0 && duplicateCount === 0) {
        parts.push('no issues found');
    }
    return parts.join(' — ');
}
//# sourceMappingURL=index.js.map