"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.gatherSecrets = gatherSecrets;
exports.gatherFileFindings = gatherFileFindings;
exports.gatherTlsBypassFindings = gatherTlsBypassFindings;
exports.gatherCodePatterns = gatherCodePatterns;
exports.gatherDepVulnsWithAvailability = gatherDepVulnsWithAvailability;
exports.gatherDepVulns = gatherDepVulns;
exports.buildSecurityAggregateForHealth = buildSecurityAggregateForHealth;
/**
 * Security finding gatherers — one function per tool, no overlap.
 *
 * Tool boundaries:
 *   gitleaks   → secrets (hardcoded credentials, API keys, private keys in source)
 *   find/git   → private key files on disk (.key, .pem), .env tracked in git
 *   semgrep    → code patterns (eval, exec, TLS, CORS, SQLi, XSS, SSRF, etc.)
 *   dispatcher → dependency CVEs unioned across every active language pack
 */
const fs = __importStar(require("fs"));
const runner_1 = require("../tools/runner");
const deadline_1 = require("../tools/deadline");
const epss_1 = require("../tools/epss");
const fingerprint_1 = require("../tools/fingerprint");
const kev_1 = require("../tools/kev");
const osv_1 = require("../tools/osv");
const reachability_1 = require("../tools/reachability");
const risk_score_1 = require("../tools/risk-score");
const upgrade_plan_resolver_1 = require("../tools/upgrade-plan-resolver");
const exclusions_1 = require("../tools/exclusions");
const walk_source_files_1 = require("../tools/walk-source-files");
const path = __importStar(require("path"));
const aggregator_1 = require("./aggregator");
const dispatcher_1 = require("../dispatcher");
const languages_1 = require("../../languages");
const descriptors_1 = require("../../languages/capabilities/descriptors");
const capabilities_1 = require("../../languages/capabilities");
// ─── dispatcher-driven secrets gather ────────────────────────────────────────
/**
 * Secrets are a global capability: one scanner (gitleaks today) runs once
 * per repo and the dispatcher aggregates its envelope through the SECRETS
 * descriptor. Exclusions + suppressions are already applied inside the
 * provider (see tools/gitleaks.ts), so this layer only maps the envelope
 * into the SecurityFinding shape used by the security report.
 */
async function gatherSecrets(cwd) {
    const result = await dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.SECRETS, (0, capabilities_1.providersFor)(descriptors_1.SECRETS, cwd));
    if (!result)
        return { findings: [], toolUsed: null };
    const findings = result.findings.map((f) => ({
        severity: f.severity,
        category: 'secret',
        cwe: 'CWE-798',
        rule: f.rule,
        title: f.title ?? `Secret detected: ${f.rule}`,
        file: f.file,
        line: f.line,
        tool: result.tool,
    }));
    return { findings, toolUsed: result.tool };
}
// ─── find/git: private key files, .env in git ───────────────────────────────
function gatherFileFindings(cwd) {
    const findings = [];
    const EXCLUDE = (0, exclusions_1.getFindExcludeFlags)(cwd, false); // don't exclude source paths for security scanning
    // Private key / cert files on disk.
    // NOTE: still a POSIX `find` (returns empty on Windows). Deliberately
    // NOT ported to walkSourceFiles yet: this scan deliberately uses the
    // broad `getFindExcludeFlags(cwd, false)` exclusion set (dirs only, NOT
    // .dxkit-ignore sourcePaths) so keys hidden in otherwise-ignored paths
    // are still flagged, and the walker can't yet express "dirs-only"
    // exclusions — porting would silently narrow a security scan. The
    // primary secret detector (gitleaks) IS cross-platform after the
    // detection fix, so the Windows gap here is a supplementary
    // file-presence heuristic, not the main secrets path.
    const keyFiles = (0, runner_1.run)(`find . \\( -name "*.key" -o -name "*.pem" \\) ${EXCLUDE}`, cwd); // posix-enum-ok: see note above; needs a walker excludeSourcePaths option first
    if (keyFiles) {
        for (const f of keyFiles.split('\n').filter((l) => l.trim())) {
            findings.push({
                severity: 'critical',
                category: 'secret',
                cwe: 'CWE-798',
                rule: 'private-key-file',
                title: `Private key or certificate file: ${f.replace('./', '')}`,
                file: f.replace('./', ''),
                line: 0,
                tool: 'find',
            });
        }
    }
    // .env tracked in git
    const envFiles = (0, runner_1.run)('git ls-files .env .env.*', cwd);
    if (envFiles) {
        for (const f of envFiles.split('\n').filter((l) => l.trim())) {
            findings.push({
                severity: 'high',
                category: 'config',
                cwe: 'CWE-798',
                rule: 'env-in-git',
                title: `.env file tracked in git: ${f}`,
                file: f,
                line: 0,
                tool: 'git',
            });
        }
    }
    return findings;
}
// ─── TLS / certificate-validation bypass gather (D045 / D034) ──────────────
/**
 * D045 (2.4.7): surface TLS-bypass idioms as first-class
 * `SecurityFinding[]` entries with file:line attribution. Each pack
 * declares its language-specific patterns via
 * `LanguageSupport.tlsBypassPatterns` (D034); this gather runs the
 * unioned alternation across every registered pack's source
 * extensions and emits one finding per matching line.
 *
 * Architecture note (why this is independent of semgrep): semgrep's
 * `p/security-audit` ruleset does not include per-language TLS-bypass
 * idioms (`ServerCertificateValidationCallback`,
 * `DangerousAcceptAnyServerCertificateValidator`,
 * `InsecureSkipVerify: true`, `danger_accept_invalid_certs`,
 * `TrustAllX509TrustManager`, `OpenSSL::SSL::VERIFY_NONE`, etc.). The
 * registry-driven per-pack patterns ARE the source of truth for these
 * checks; both the health-side `tlsDisabledCount` metric and the
 * standalone vuln-scan Code Findings table flow through the same
 * patterns. False-positive rate is near zero — these are tight
 * class/method tokens, not loose word matches.
 *
 * Pre-D045 the .NET WinForms benchmark surfaced `tlsDisabledCount: 1` in
 * `gatherGenericMetrics` (via `countTlsBypassLines`), but the
 * standalone vuln scan's Code Findings table reported `_Sources:
 * (none)_` with all zeros — the count never reached the standalone
 * scan because TLS-bypass wasn't a first-class finding source. This
 * gather closes that gap.
 *
 * Severity assignment: `high`. CWE: 295 (Improper Certificate
 * Validation).
 *
 * Empty patterns array → returns []. Empty grep output → returns [].
 * Both are legitimate "no TLS-bypass idioms in this codebase" states.
 */
/**
 * G_v4_7 (2.4.7): route TLS-bypass discovery through the canonical
 * walker + per-file in-process line scan. Eliminates the `grep -rnEf`
 * shell path (no maxBuffer ceiling, no per-finding shell escaping).
 * D074 closure: skip comment lines so a commented-out
 * `// NODE_TLS_REJECT_UNAUTHORIZED=0` no longer renders as a HIGH
 * SecurityFinding (the platform vuln-scan false-positive class).
 *
 * `includeTests: true` preserves pre-migration scope — TLS-bypass
 * idioms inside test fixtures were detected before; still are.
 */
function gatherTlsBypassFindings(cwd) {
    const patterns = (0, languages_1.allTlsBypassPatterns)();
    if (patterns.length === 0)
        return [];
    const compiled = patterns.map((p) => new RegExp(p));
    const files = (0, walk_source_files_1.walkSourceFiles)(cwd, { includeTests: true });
    const findings = [];
    for (const relPath of files) {
        let content;
        try {
            content = fs.readFileSync(path.join(cwd, relPath), 'utf-8');
        }
        catch {
            continue;
        }
        const lines = content.split('\n');
        const syntax = (0, walk_source_files_1.commentSyntaxFor)(relPath);
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            if (syntax !== 'none' && (0, walk_source_files_1.isCommentLine)(line, syntax))
                continue;
            let matched = false;
            for (const re of compiled) {
                re.lastIndex = 0;
                if (re.test(line)) {
                    matched = true;
                    break;
                }
            }
            if (!matched)
                continue;
            const trimmed = line.trim();
            const snippet = trimmed.length > 100 ? `${trimmed.slice(0, 97)}…` : trimmed;
            findings.push({
                severity: 'high',
                category: 'code',
                cwe: 'CWE-295',
                rule: 'tls-validation-disabled',
                title: `TLS / certificate validation bypass: ${snippet}`,
                file: relPath,
                line: i + 1,
                tool: 'tls-bypass-registry',
            });
        }
    }
    return findings;
}
// ─── dispatcher-driven codePatterns gather ──────────────────────────────────
/**
 * Code-pattern findings are a global capability: the CODE_PATTERNS
 * dispatcher routes to `semgrepProvider` (tools/semgrep.ts) which
 * applies exclusions, suppressions, and the low-confidence filter
 * internally. This layer only reshapes the envelope into
 * `SecurityFinding[]` for the security report.
 */
async function gatherCodePatterns(cwd) {
    const result = await dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.CODE_PATTERNS, (0, capabilities_1.providersFor)(descriptors_1.CODE_PATTERNS, cwd));
    if (!result)
        return { findings: [], toolUsed: null };
    const findings = result.findings.map((f) => ({
        severity: f.severity,
        category: 'code',
        cwe: f.cwe,
        rule: f.rule,
        title: f.title,
        file: f.file,
        line: f.line,
        tool: result.tool,
    }));
    return { findings, toolUsed: result.tool };
}
// ─── dependency CVEs via the capabilities dispatcher ────────────────────────
const EMPTY_DEP_VULNS = {
    critical: 0,
    high: 0,
    medium: 0,
    low: 0,
    total: 0,
    tool: null,
    findings: [],
    // No active pack → genuinely "nothing to scan" (not "scan failed"). The
    // security scorer should not cap the dimension in this case; e.g. a
    // pure-static-asset repo with no language packs active legitimately
    // has no deps to audit. available=true preserves this.
    available: true,
    unavailableReason: '',
};
/**
 * Aggregates dependency vulnerabilities across every active language pack
 * via the capability dispatcher. Replaces the prior hardcoded `npm audit`
 * implementation that silently ignored Python/Go/Rust/C# deps.
 *
 * Returns `EMPTY_DEP_VULNS` when no active pack exposes a depVulns
 * provider, or when every provider returned null (no tool installed
 * / nothing to audit).
 */
/**
 * Shared primitive for availability-aware dep-vuln aggregation. Used by
 * both `gatherDepVulns` (standalone scan + BoM, with enrichment) and
 * `gatherCapabilityReport` in health.ts (no enrichment). Bypassing the
 * dispatcher is the whole point — the dispatcher's `gather()` path
 * collapses every non-success outcome to null, which makes the scorer
 * blind to "tool unavailable" vs "no findings" (the F4
 * customer-credibility lie observed on the .NET WinForms benchmark).
 * Calling `gatherOutcome` directly preserves
 * the discriminant, then we aggregate via the existing DEP_VULNS
 * descriptor's aggregator.
 *
 * Returned envelope is null only when NO success outcomes occurred;
 * `available` is false when at least one active pack returned
 * `unavailable`. `no-manifest` outcomes do NOT degrade availability —
 * polyglot repos where one pack activates but has nothing to scan are
 * a clean "we checked, found nothing here" state.
 */
async function gatherDepVulnsWithAvailability(cwd) {
    const activePacks = (0, languages_1.detectActiveLanguages)(cwd).filter((l) => l.capabilities?.depVulns);
    if (activePacks.length === 0) {
        return { envelope: null, available: true, unavailableReason: '' };
    }
    // Every per-pack gatherOutcome is wrapped in a deadline (mirrors the
    // dispatcher's per-provider deadline) so a single pack that hangs
    // can't keep the cross-pack `Promise.allSettled` pending forever.
    // A stall is materialised as an `unavailable` outcome with a
    // deadline reason, so the existing availability machinery surfaces
    // it through `toolsUnavailable` without any consumer change.
    const outcomes = await Promise.allSettled(activePacks.map((l) => (0, deadline_1.withDeadline)(l.capabilities.depVulns.gatherOutcome(cwd), deadline_1.DEFAULT_PROVIDER_DEADLINE_MS).then((deadlineOutcome) => {
        if (deadlineOutcome.stalled) {
            const seconds = Math.round(deadlineOutcome.stalledMs / 1000);
            process.stderr.write(`[dxkit] depVulns provider "${l.id}" stalled after >${seconds}s (deadline) — treating as unavailable\n`);
            return {
                kind: 'unavailable',
                reason: `stalled at >${seconds}s (deadline)`,
            };
        }
        return deadlineOutcome.value;
    })));
    const successEnvelopes = [];
    let firstUnavailable = null;
    for (let i = 0; i < outcomes.length; i++) {
        const r = outcomes[i];
        if (r.status === 'rejected') {
            if (!firstUnavailable) {
                firstUnavailable = {
                    pack: activePacks[i].id,
                    reason: `provider threw: ${r.reason?.message ?? 'unknown error'}`,
                };
            }
            continue;
        }
        const outcome = r.value;
        if (outcome.kind === 'success') {
            successEnvelopes.push(outcome.envelope);
        }
        else if (outcome.kind === 'unavailable' && !firstUnavailable) {
            firstUnavailable = { pack: activePacks[i].id, reason: outcome.reason };
        }
    }
    const envelope = successEnvelopes.length > 0 ? descriptors_1.DEP_VULNS.aggregate(successEnvelopes) : null;
    // G_v4_8 (2.4.7 Phase C1.3): stamp fingerprints on the envelope's
    // findings here, in the shared primitive, so BOTH the health path
    // and the enrichment path (`gatherDepVulns`) produce fingerprint-
    // stamped findings. The aggregator's dep-side dedup needs the
    // fingerprint key; without it, unstamped findings each get a
    // synthetic unique key (no dedup), and health's
    // `depBySeverity` / `dependencyAdvisoryUniqueCount` would drift
    // from vuln-scan's. Idempotent — re-stamping in `gatherDepVulns`
    // produces the same hashes.
    if (envelope?.findings) {
        (0, fingerprint_1.stampFingerprints)(envelope.findings);
    }
    return {
        envelope,
        available: firstUnavailable === null,
        unavailableReason: firstUnavailable
            ? `${firstUnavailable.pack}: ${firstUnavailable.reason}`
            : '',
    };
}
async function gatherDepVulns(cwd) {
    // D025b (2.4.7): delegates to `gatherDepVulnsWithAvailability` for
    // the availability-aware aggregation; this function adds the
    // enrichment passes (EPSS, KEV, reachability, risk scoring) on top.
    // Health audit calls the shared primitive directly without enrichment;
    // standalone vuln scan + BoM call this function for the enriched path.
    const { envelope, available, unavailableReason } = await gatherDepVulnsWithAvailability(cwd);
    if (!envelope) {
        return {
            ...EMPTY_DEP_VULNS,
            available,
            unavailableReason,
        };
    }
    // Cross-pack EPSS enrichment. Every pack's dep-vuln provider emits
    // findings with an `id` + optional `aliases` list; we hoist CVE IDs
    // across the whole batch, fetch once, then attach `epssScore` in
    // place. Done here rather than per-pack so (a) one session cache
    // serves all packs, (b) the EPSS endpoint sees at most one batched
    // request per analyzer run, (c) non-CVE primaries (GHSA, RUSTSEC,
    // GO-YYYY-NNNN) fall back to aliases uniformly.
    //
    // Two-step lookup: npm-audit only surfaces GHSA IDs with no CVE
    // aliases. When `extractCveId` comes up empty, we fall back to
    // OSV.dev's `/v1/vulns/<GHSA>` which returns a properly-populated
    // alias list including the CVE. One OSV roundtrip resolves the
    // whole batch; one EPSS roundtrip scores them all.
    const findings = envelope.findings ?? [];
    // Stamp durable identity on every finding before enrichment. The hash
    // inputs are package/version/id only, so stamping is independent of
    // EPSS/KEV/reachability results — keeps `fingerprint` stable across
    // runs even if enrichment tooling changes underneath.
    (0, fingerprint_1.stampFingerprints)(findings);
    if (findings.length > 0) {
        const cveByFinding = new Map();
        const needsAliasLookup = [];
        for (let i = 0; i < findings.length; i++) {
            const direct = (0, epss_1.extractCveId)(findings[i]);
            if (direct) {
                cveByFinding.set(i, direct);
            }
            else {
                needsAliasLookup.push({ idx: i, primary: findings[i].id });
            }
        }
        if (needsAliasLookup.length > 0) {
            const aliasMap = await (0, osv_1.resolveAliases)(needsAliasLookup.map((x) => x.primary));
            for (const { idx, primary } of needsAliasLookup) {
                const aliases = aliasMap.get(primary) ?? [];
                const cve = aliases.find((a) => a.startsWith('CVE-'));
                if (cve)
                    cveByFinding.set(idx, cve);
            }
        }
        if (cveByFinding.size > 0) {
            const uniqueCves = [...new Set(cveByFinding.values())];
            // EPSS + KEV run in parallel — one roundtrip each, independent
            // endpoints. KEV catalog is a single bulk fetch (~200KB, 1300
            // entries), so subsequent lookups in the same session are free.
            const [scores, kevHits] = await Promise.all([(0, epss_1.enrichEpss)(uniqueCves), (0, kev_1.enrichKev)(uniqueCves)]);
            for (const [idx, cve] of cveByFinding) {
                const score = scores.get(cve);
                if (score !== undefined)
                    findings[idx].epssScore = score;
                if (kevHits.has(cve))
                    findings[idx].kev = true;
            }
        }
        // Reachability — does the repo's source actually import any of
        // these vulnerable packages? Dispatches the IMPORTS capability
        // (which packs populate from their per-file specifier extraction)
        // once, unions into a name set, then marks every finding. When
        // no pack contributes imports (no source files / all packs
        // declined), leaves `reachable` unset rather than mass-classify
        // everything as false.
        //
        // Intentionally NOT stack-filtered (post-D010): the BoM aggregates
        // across multiple project roots (e.g. `test/fixtures/benchmarks/*`)
        // and findings come from all languages. Reachability needs to walk
        // every pack's imports — filtering by the outer cwd's active packs
        // would silently drop cross-language reachability for findings
        // attributed to inactive-pack roots. Each provider already returns
        // null when its source files don't exist; the cost is a few empty
        // `find` walks at most.
        const importsProviders = (0, capabilities_1.providersFor)(descriptors_1.IMPORTS);
        if (importsProviders.length > 0) {
            const importsEnvelope = await dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.IMPORTS, importsProviders);
            if (importsEnvelope && importsEnvelope.extracted.size > 0) {
                const reachable = (0, reachability_1.buildReachablePackageSet)(importsEnvelope);
                (0, reachability_1.markReachable)(findings, reachable);
            }
        }
        // Cross-pack upgrade-plan resolver (Phase 10h.6.4). Runs after
        // per-pack Tier-2 tools have stamped what they can, and before
        // risk scoring so the composite riskScore can factor in the
        // "actionable" bit (future 10h.9.2 CI gate uses it too). Fills
        // gaps by (a) reconciling advisories across plans' `patches[]`
        // lists and (b) parsing the npm-audit transitive-fix free-text
        // template into a structured plan when no tool produced one.
        (0, upgrade_plan_resolver_1.resolveTransitiveUpgradePlans)(findings);
        // Composite riskScore = f(cvss, epss, kev, reachable). Runs last
        // so every signal is populated. Formula is documented in
        // risk-score.ts; skipped for findings without CVSS so we don't
        // fabricate severity from partial data.
        (0, risk_score_1.scoreFindings)(findings);
    }
    const { critical, high, medium, low } = envelope.counts;
    return {
        critical,
        high,
        medium,
        low,
        total: critical + high + medium + low,
        tool: envelope.tool,
        findings,
        // Even with successful envelopes from some packs, ONE pack returning
        // unavailable means the overall scan was partial — cap honesty
        // applies. The .NET WinForms benchmark shape post-D025f (sub-branch #3) will have
        // csharp surfacing real CVEs AND any other unavailable pack still
        // capping; that's the architecturally-correct outcome.
        available,
        unavailableReason,
    };
}
// ─── Shared aggregate builder for health (G_v4_8 / C1.3) ─────────────────────
/**
 * Build the canonical `SecurityAggregate` from inputs available to the
 * health analyzer. Re-uses the capability envelopes already gathered by
 * `gatherCapabilityReport` (no double-shells — dispatcher cache hits
 * are free), additionally invoking the two finders not represented in
 * the capability layer (TLS-bypass-registry walk, file findings for
 * private keys + `.env`-in-git).
 *
 * D086 closure foundation: health's `scoreSecurityDimension` reads
 * from this aggregate via `c.securityAggregate?.codeBySeverity`,
 * which is the SAME field the standalone vuln-scan reads after C1.2.
 * Two consumers, one source — no drift possible.
 */
async function buildSecurityAggregateForHealth(cwd, secrets, codePatterns, depVulnsEnvelope, depVulnsAvailable, depVulnsUnavailableReason) {
    // The two gathers not represented in CapabilityReport (vuln-scan-only).
    // Both are cheap: `gatherTlsBypassFindings` is a JS line-scan via
    // `walkSourceFiles`; `gatherFileFindings` is one `find` + one `git
    // ls-files`. Total ~0.5s on a 500-file repo.
    const tlsBypass = gatherTlsBypassFindings(cwd);
    const fileFindings = gatherFileFindings(cwd);
    const secretFindings = secrets
        ? secrets.findings.map((f) => ({
            severity: f.severity,
            category: 'secret',
            cwe: 'CWE-798',
            rule: f.rule,
            title: f.title ?? `Secret detected: ${f.rule}`,
            file: f.file,
            line: f.line,
            tool: secrets.tool,
        }))
        : [];
    const codeFindings = codePatterns
        ? codePatterns.findings.map((f) => ({
            severity: f.severity,
            category: 'code',
            cwe: f.cwe,
            rule: f.rule,
            title: f.title,
            file: f.file,
            line: f.line,
            tool: codePatterns.tool,
        }))
        : [];
    return (0, aggregator_1.buildSecurityAggregate)({
        secrets: { findings: secretFindings, toolUsed: secrets?.tool ?? null },
        fileFindings,
        codePatterns: { findings: codeFindings, toolUsed: codePatterns?.tool ?? null },
        tlsBypass,
        tlsBypassPatternCount: (0, languages_1.allTlsBypassPatterns)().length,
        depVulns: {
            findings: depVulnsEnvelope?.findings ?? [],
            tool: depVulnsEnvelope?.tool ?? null,
            available: depVulnsAvailable,
            unavailableReason: depVulnsUnavailableReason,
        },
    });
}
//# sourceMappingURL=gather.js.map