"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseOsvScannerFindings = parseOsvScannerFindings;
exports.gatherOsvScannerDepVulnsResult = gatherOsvScannerDepVulnsResult;
/**
 * osv-scanner against any OSV ecosystem — shared across language packs
 * that use osv-scanner as their canonical depVulns source. CLAUDE.md
 * rule #2 — the gather function lives once.
 *
 * History: extracted from `src/languages/kotlin.ts` in 10k.1.4 (Phase
 * 10k.1 SSOT validation), originally Maven-only. Generalized to all
 * OSV ecosystems in 10k.2.6a (Ruby pack work) — caller passes the
 * ecosystem string + manifest candidate list, parser filters
 * accordingly so polyglot repos don't double-count across packs.
 *
 * Current consumers:
 *   - kotlin pack — `Maven` ecosystem, gradle.lockfile + pom.xml + verification-metadata.xml
 *   - java pack — `Maven` ecosystem (same manifest set)
 *   - ruby pack — `RubyGems` ecosystem, Gemfile.lock
 *
 * osv-scanner is the established multi-ecosystem scanner; no Tier-1
 * native equivalent exists for several of the ecosystems above
 * (CLAUDE.md rule #5 — bundler-audit's JSON is unstable, so Ruby
 * intentionally uses osv-scanner-only rather than dual-source).
 * The typescript pack's `osv-scanner-fix.ts` uses the `fix`
 * subcommand for upgrade planning — different mode, no shared logic.
 *
 * Manifest gating: caller supplies the candidate list. First
 * existing candidate wins. Without any of them, returns
 * `tool-missing` (matches python/csharp's manifest-gating pattern).
 */
const osv_1 = require("./osv");
const runner_1 = require("./runner");
const tool_registry_1 = require("./tool-registry");
/**
 * Pure parser for osv-scanner v2.x JSON output, scoped to a single
 * ecosystem. Other ecosystems are filtered out so polyglot repos
 * don't double-count: each pack handles its own ecosystem (typescript
 * → npm, python → PyPI, kotlin/java → Maven, ruby → RubyGems, etc.).
 *
 * The ecosystem parameter is matched against the OSV record's
 * `package.ecosystem` field verbatim — use the exact strings OSV
 * emits (`'Maven'`, `'RubyGems'`, `'PyPI'`, `'npm'`, `'Go'`, etc.).
 *
 * Returns counts + findings + the raw OSV vuln records for downstream
 * CVSS resolution. Exported for unit tests.
 */
function parseOsvScannerFindings(raw, ecosystem, packId) {
    const counts = { critical: 0, high: 0, medium: 0, low: 0 };
    const findings = [];
    const vulnsForCvss = [];
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return { counts, findings, vulnsForCvss };
    }
    // Dedup at the source: osv-scanner can list the same advisory twice
    // when a transitive dep is reachable through multiple top-level deps.
    // Same (package, version, id) → same fingerprint, so collapse here.
    const seen = new Set();
    for (const result of data.results ?? []) {
        for (const pkg of result.packages ?? []) {
            if (pkg.package?.ecosystem !== ecosystem)
                continue;
            const pkgName = pkg.package.name ?? 'unknown';
            const pkgVersion = pkg.package.version;
            for (const vuln of pkg.vulnerabilities ?? []) {
                if (!vuln.id)
                    continue;
                const dedupKey = `${pkgName}\0${pkgVersion ?? ''}\0${vuln.id}`;
                if (seen.has(dedupKey))
                    continue;
                seen.add(dedupKey);
                const sev = (0, osv_1.classifyOsvSeverity)(vuln);
                const tier = sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low'
                    ? sev
                    : 'medium';
                counts[tier]++;
                const cvss = (0, osv_1.extractOsvCvssScore)(vuln);
                const aliases = (vuln.aliases ?? []).filter((a) => a && a.length > 0);
                const finding = {
                    id: vuln.id,
                    package: pkgName,
                    installedVersion: pkgVersion,
                    tool: 'osv-scanner',
                    severity: tier,
                };
                // G_v4_4 (2.4.7): stamp the producing pack so `buildUpgradeCommand`
                // can dispatch to the right `LanguageSupport.upgradeCommand` without
                // a hardcoded switch on `tool`. Caller passes the pack id; absent
                // (`undefined`) only on legacy paths we haven't migrated yet.
                if (packId)
                    finding.packId = packId;
                if (cvss !== null)
                    finding.cvssScore = cvss;
                if (aliases.length > 0)
                    finding.aliases = aliases;
                if (vuln.summary)
                    finding.summary = vuln.summary;
                // D042: surface the patch version when OSV's `affected[].
                // ranges[].events[].fixed` is populated. This is the customer's
                // actionable next-step (e.g. "upgrade Newtonsoft.Json from
                // 9.0.1 to 13.0.1 to clear GHSA-5crp-9r3c-p9vr"). Pre-D042 the
                // standalone scan rendered `Fix: —` for every osv-scanner-
                // sourced finding because this field went unread.
                const fixVersion = (0, osv_1.extractOsvFixVersion)(vuln);
                if (fixVersion)
                    finding.fixedVersion = fixVersion;
                // OSV.dev hosts a canonical page per id — synthesize when the
                // record's `references[]` is empty, otherwise keep the
                // tool-supplied URLs.
                const refUrls = (vuln.references ?? []).map((r) => r.url).filter((u) => !!u);
                finding.references =
                    refUrls.length > 0 ? refUrls : [`https://osv.dev/vulnerability/${vuln.id}`];
                findings.push(finding);
                vulnsForCvss.push({
                    primaryId: vuln.id,
                    embeddedCvss: cvss,
                    aliases,
                });
            }
        }
    }
    return { counts, findings, vulnsForCvss };
}
/**
 * Single source of truth for osv-scanner-driven dep-vuln gathering.
 * Caller supplies:
 *   - cwd: project root
 *   - source: pack id for envelope attribution (currently reserved —
 *     see note at end of function)
 *   - ecosystem: OSV ecosystem string (`'Maven'`, `'RubyGems'`, ...)
 *   - manifestCandidates: ordered list of manifest filenames to probe.
 *     First existing one is passed via `--lockfile`. Lockfiles
 *     preferred over higher-level manifests (kotlin: gradle.lockfile
 *     before pom.xml; ruby: Gemfile.lock).
 *
 * `scan source --lockfile <path>` is the v2.x form. JSON output to
 * stdout. Exit code is non-zero when findings exist — we ignore the
 * exit code and parse the JSON regardless (run() already swallows
 * non-zero exits cleanly via execSync's catch).
 *
 * CVSS alias-fallback: osv-scanner ships CVSS vectors when present,
 * but advisory data quality varies by ecosystem — some carry only
 * `database_specific.severity` strings. resolveCvssScores looks up
 * via CVE alias when the primary record lacks a vector.
 */
async function gatherOsvScannerDepVulnsResult(cwd, packId, ecosystem, manifestCandidates) {
    let manifest = null;
    for (const rel of manifestCandidates) {
        if ((0, runner_1.fileExists)(cwd, rel)) {
            manifest = rel;
            break;
        }
    }
    if (!manifest) {
        return {
            kind: 'no-manifest',
            reason: `no lockfile found (looked for: ${manifestCandidates.join(', ')})`,
        };
    }
    const scanner = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['osv-scanner'], cwd);
    if (!scanner.available || !scanner.path) {
        return { kind: 'unavailable', reason: 'osv-scanner not installed' };
    }
    const raw = (0, runner_1.run)(`${scanner.path} scan source --lockfile ${manifest} --format json`, cwd, 180000);
    if (!raw)
        return { kind: 'unavailable', reason: 'osv-scanner produced no output' };
    const { counts, findings, vulnsForCvss } = parseOsvScannerFindings(raw, ecosystem, packId);
    if (findings.length > 0) {
        const resolved = await (0, osv_1.resolveCvssScores)(vulnsForCvss);
        for (const f of findings) {
            const score = resolved.get(f.id);
            if (score !== null && score !== undefined)
                f.cvssScore = score;
        }
    }
    const envelope = {
        schemaVersion: 1,
        tool: 'osv-scanner',
        enrichment: 'osv.dev',
        counts,
        findings,
    };
    // G_v4_4 (2.4.7): `packId` is forwarded into `parseOsvScannerFindings`
    // so each finding carries the producing pack, which `buildUpgradeCommand`
    // dispatches on. Envelope-level `tool: 'osv-scanner'` stays as the
    // tool-attribution string used in `toolsUsed`.
    return { kind: 'success', envelope };
}
//# sourceMappingURL=osv-scanner-deps.js.map