"use strict";
/**
 * Semgrep integration — static code-pattern scanning.
 *
 * The `semgrepProvider` is registered in `GLOBAL_CAPABILITIES.codePatterns`
 * (src/languages/capabilities/global.ts). Semgrep runs once per repo with
 * a union of rulesets from every active language pack's `semgrepRulesets`
 * declaration plus the baseline `p/security-audit` OWASP ruleset.
 *
 * The provider does not run if no rulesets resolve (pure C# or similar
 * repos today — their pack declares `semgrepRulesets: []`). Adding
 * rulesets in future is purely declarative: a pack lists them, this
 * provider picks them up via `detectActiveLanguages(cwd)`.
 */
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.semgrepProvider = void 0;
exports.extractCwe = extractCwe;
exports.gatherSemgrepResult = gatherSemgrepResult;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const languages_1 = require("../../languages");
const exclusions_1 = require("./exclusions");
const paths_1 = require("./paths");
const runner_1 = require("./runner");
const suppressions_1 = require("./suppressions");
const tool_registry_1 = require("./tool-registry");
/**
 * Map semgrep's severity + impact to the project's four-tier model.
 * Priority: rule metadata `impact` (most meaningful — rule authors
 * tier by business impact) → fall back to semgrep's `severity`.
 */
/**
 * Normalize semgrep's `metadata.cwe` into a single CWE identifier.
 *
 * Why: semgrep rule authors write `cwe:` in YAML as either a scalar
 * (`cwe: "CWE-295: Improper Certificate Validation"`) or a list
 * (`cwe: ["CWE-295: ..."]`). Both shapes pass through semgrep's JSON
 * output unchanged. Pre-fix this code did `metadata?.cwe?.[0]` which
 * silently returned the first *character* of the scalar form (e.g.
 * "C" for "CWE-295: ..."). D094 surfaced this on `bypass-tls-
 * verification` rule output.
 */
function extractCwe(cwe) {
    if (!cwe)
        return '';
    const raw = Array.isArray(cwe) ? cwe[0] : cwe;
    if (typeof raw !== 'string')
        return '';
    return raw.split(':')[0].trim();
}
function mapSemgrepSeverity(sgSeverity, impact) {
    const imp = (impact || '').toUpperCase();
    if (imp === 'HIGH')
        return sgSeverity === 'ERROR' ? 'critical' : 'high';
    if (imp === 'MEDIUM')
        return 'medium';
    if (imp === 'LOW')
        return 'low';
    if (sgSeverity === 'ERROR')
        return 'high';
    if (sgSeverity === 'WARNING')
        return 'medium';
    return 'low';
}
/**
 * Collect semgrep rulesets to invoke for this repo.
 *
 * Base `p/security-audit` covers OWASP Top 10 across all languages.
 * Each active pack contributes its own rulesets declaratively via
 * `LanguageSupport.semgrepRulesets` — the provider unions them and
 * deduplicates so switching a pack's rulesets is a one-line edit.
 */
function collectRulesets(cwd) {
    const rulesets = new Set(['p/security-audit']);
    for (const pack of (0, languages_1.detectActiveLanguages)(cwd)) {
        for (const r of pack.semgrepRulesets)
            rulesets.add(r);
    }
    return [...rulesets];
}
/**
 * Single source of truth for the semgrep invocation. Consumed by
 * `semgrepProvider` (capability dispatcher).
 *
 * Failure-mode honesty: when semgrep doesn't produce a parseable
 * report, the returned `reason` distinguishes between:
 *   - timeout (we hit our wall-clock budget — the customer probably
 *     wants to install nothing and instead either prune the scan
 *     scope via `.dxkit-ignore` or bump the timeout)
 *   - non-zero exit with a captured stderr first line (semgrep
 *     itself complained — surface its complaint)
 *   - the historical fallback "no output" (rare now; means stderr
 *     was empty AND exit was zero AND the report file was missing)
 *
 * Pre-fix every failure collapsed to "no output," masking
 * resource-contention deaths (parallel jscpd + graphify + semgrep
 * on a 700-file repo OOM-killing the youngest), timeouts, and
 * config-parse errors with the same useless string. Switched to
 * runDetached so we capture stderr + exit code + timeout signal
 * separately, and so the wall-clock-deadline kill cleans up
 * grandchildren (semgrep's internal worker pool).
 */
async function gatherSemgrepResult(cwd) {
    const status = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS.semgrep, cwd);
    if (!status.available || !status.path)
        return { kind: 'unavailable', reason: 'not installed' };
    const rulesets = collectRulesets(cwd);
    if (rulesets.length === 0)
        return { kind: 'unavailable', reason: 'no rulesets' };
    const reportPath = path.join(os.tmpdir(), `dxkit-semgrep-${Date.now()}.json`);
    const args = ['scan'];
    for (const r of rulesets)
        args.push('--config', r);
    args.push('--json', '--quiet', '--output', reportPath);
    // getSemgrepExcludeFlags returns a single space-separated string
    // shaped for execSync (`--exclude foo --exclude bar`). Split it
    // into the array form runDetached expects.
    const excludeFlagString = (0, exclusions_1.getSemgrepExcludeFlags)(cwd);
    if (excludeFlagString) {
        for (const tok of excludeFlagString.split(/\s+/).filter((t) => t.length > 0)) {
            args.push(tok);
        }
    }
    args.push(cwd);
    const outcome = await (0, runner_1.runDetached)(status.path, args, { cwd, timeoutMs: 300000 });
    let raw;
    try {
        raw = fs.readFileSync(reportPath, 'utf-8');
    }
    catch {
        raw = '';
    }
    // Cleanup: best-effort; failure here is non-fatal.
    try {
        fs.unlinkSync(reportPath);
    }
    catch {
        /* file already gone or never written — fine */
    }
    if (!raw) {
        if (outcome.timedOut) {
            return {
                kind: 'unavailable',
                reason: 'timed out at 300s (try narrowing scan scope via .dxkit-ignore)',
            };
        }
        const stderrFirstLine = outcome.stderr
            .split('\n')
            .map((l) => l.trim())
            .find((l) => l.length > 0);
        if (outcome.code !== 0 && outcome.code !== null) {
            const ctx = stderrFirstLine ? ` (stderr: ${stderrFirstLine})` : '';
            return { kind: 'unavailable', reason: `exit code ${outcome.code}${ctx}` };
        }
        if (stderrFirstLine) {
            return { kind: 'unavailable', reason: `no output (stderr: ${stderrFirstLine})` };
        }
        return { kind: 'unavailable', reason: 'no output' };
    }
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return { kind: 'unavailable', reason: 'parse error' };
    }
    if (!Array.isArray(data.results)) {
        const envelope = {
            schemaVersion: 1,
            tool: 'semgrep',
            findings: [],
            suppressedCount: 0,
        };
        return { kind: 'success', envelope };
    }
    const raw_findings = data.results
        // Skip LOW confidence (high false-positive rate); preserves the
        // pre-10e behaviour exactly.
        .filter((r) => (r.extra.metadata?.confidence || '').toUpperCase() !== 'LOW')
        .map((r) => ({
        severity: mapSemgrepSeverity(r.extra.severity, r.extra.metadata?.impact),
        rule: r.check_id.split('.').slice(-1)[0],
        title: r.extra.message.split('\n')[0].slice(0, 200),
        cwe: extractCwe(r.extra.metadata?.cwe),
        file: (0, paths_1.toProjectRelative)(cwd, r.path),
        line: r.start.line,
    }));
    // Apply `.dxkit-suppressions.json` so known false positives can be
    // dropped without editing the ruleset.
    const suppressions = (0, suppressions_1.loadSuppressions)(cwd);
    const { kept, suppressed } = (0, suppressions_1.applySuppressions)(raw_findings, suppressions.semgrep, (f) => f.rule, (f) => f.file);
    const envelope = {
        schemaVersion: 1,
        tool: 'semgrep',
        findings: kept,
        suppressedCount: suppressed.length,
    };
    return { kind: 'success', envelope };
}
/**
 * Capability-shaped provider. Registered in
 * `src/languages/capabilities/global.ts:GLOBAL_CAPABILITIES.codePatterns`.
 */
// Exposes the underlying outcome via `gatherOutcome` so the dispatcher
// captures semgrep's actual failure reason (timeout / exit code /
// stderr first line) into `DispatchOutcome.skipReasons`. Without it,
// every failure modes collapses to the same generic "attempted but
// produced no output" prose at the renderer layer.
exports.semgrepProvider = {
    source: 'semgrep',
    async gather(cwd) {
        const outcome = await gatherSemgrepResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherSemgrepResult(cwd);
    },
};
//# sourceMappingURL=semgrep.js.map