// engine/security/cve-refresher.ts — SPEC-773
// Auto-refresh known-cves.json from OSV.dev public API.
import { stat, readFile, mkdir } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { globalDataDir } from '../../storage/base-store.js';
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
export const CVE_DB_PATH = resolve(MODULE_DIR, '../../config/known-cves.json');
const OSV_API = 'https://api.osv.dev/v1/query';
const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
/** Check if known-cves.json is older than 7 days. */
export async function isCveDatabaseStale() {
    try {
        const s = await stat(CVE_DB_PATH);
        const ageMs = Date.now() - s.mtimeMs;
        return { stale: ageMs > STALE_THRESHOLD_MS, ageDays: Math.floor(ageMs / 86400000) };
    }
    catch {
        return { stale: true, ageDays: 999 };
    }
}
function toEntry(vuln, pkgName, ecosystem) {
    const cveId = vuln.aliases?.find((a) => a.startsWith('CVE-'));
    if (!cveId) {
        return null;
    }
    let fixVersion;
    outer: for (const aff of vuln.affected ?? []) {
        for (const range of aff.ranges ?? []) {
            for (const ev of range.events ?? []) {
                if (ev.fixed) {
                    fixVersion = ev.fixed;
                    break outer;
                }
            }
        }
    }
    const score = parseFloat(vuln.affected?.[0]?.severity?.[0]?.score ?? '0');
    const severity = score >= 7 ? 'critical' : 'warning';
    return {
        package: pkgName,
        ecosystem,
        affectedVersions: fixVersion ? `<${fixVersion}` : 'unknown',
        cveId,
        severity,
        description: vuln.summary ?? '',
        ...(fixVersion ? { fixVersion } : {}),
    };
}
/**
 * Refresh known-cves.json from OSV.dev for the given ecosystems.
 * Queries each package already in the database to get up-to-date CVE data.
 * Writes updated JSON via atomicWriteFile. Records refresh in cve-refresh-log.json.
 */
export async function refreshCveDatabase(ecosystems) {
    let currentDb;
    try {
        const raw = await readFile(CVE_DB_PATH, 'utf-8');
        currentDb = JSON.parse(raw);
    }
    catch {
        currentDb = { lastUpdated: '', vulnerabilities: [] };
    }
    // Build package list per ecosystem for ecosystems being refreshed (AC-5: ecosystem-scoped)
    const byEcosystem = new Map();
    for (const vuln of currentDb.vulnerabilities) {
        if (!ecosystems.includes(vuln.ecosystem)) {
            continue;
        }
        const set = byEcosystem.get(vuln.ecosystem) ?? new Set();
        set.add(vuln.package);
        byEcosystem.set(vuln.ecosystem, set);
    }
    // Query OSV.dev per package with ecosystem filter (not a generic all-ecosystems fetch)
    const freshVulns = [];
    for (const [ecosystem, packages] of byEcosystem) {
        for (const pkgName of packages) {
            try {
                const resp = await fetch(OSV_API, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ package: { name: pkgName, ecosystem } }),
                });
                if (!resp.ok) {
                    continue;
                }
                const data = (await resp.json());
                for (const vuln of data.vulns ?? []) {
                    const entry = toEntry(vuln, pkgName, ecosystem);
                    if (entry) {
                        freshVulns.push(entry);
                    }
                }
            }
            catch {
                // per-package error — skip and continue
            }
        }
    }
    // Preserve untouched ecosystems; replace refreshed ones with fresh data (or keep stale if
    // OSV returned nothing new for that ecosystem)
    const untouched = currentDb.vulnerabilities.filter((v) => !ecosystems.includes(v.ecosystem));
    const refreshedEcos = new Set(freshVulns.map((v) => v.ecosystem));
    const staleKept = currentDb.vulnerabilities.filter((v) => ecosystems.includes(v.ecosystem) && !refreshedEcos.has(v.ecosystem));
    const merged = [...untouched, ...staleKept, ...freshVulns];
    const updatedDb = {
        lastUpdated: new Date().toISOString().slice(0, 10),
        vulnerabilities: merged,
    };
    await atomicWriteFile(CVE_DB_PATH, JSON.stringify(updatedDb, null, 2));
    // Write refresh log (best-effort — non-fatal)
    try {
        const dir = globalDataDir();
        await mkdir(dir, { recursive: true });
        const log = {
            refreshedAt: new Date().toISOString(),
            source: 'osv.dev',
            ecosystems,
            entryCount: merged.length,
        };
        await atomicWriteFile(`${dir}/cve-refresh-log.json`, JSON.stringify(log, null, 2));
    }
    catch {
        // log write failure is non-fatal
    }
}
//# sourceMappingURL=cve-refresher.js.map