/**
 * Severity Overrides Configuration
 *
 * Allows users to customize finding severities via `.vaspera/severity-overrides.yaml`.
 *
 * Example configuration:
 * ```yaml
 * overrides:
 *   - ruleId: "semgrep:javascript.lang.security.audit.detect-non-literal-require"
 *     severity: "low"
 *     reason: "False positive in our codebase"
 *
 *   - scanner: "npm-audit"
 *     cwe: "CWE-1321"
 *     severity: "medium"
 *     reason: "Prototype pollution not exploitable in our context"
 *
 *   - file: "tests/**"
 *     severity: "info"
 *     reason: "Test files have relaxed security requirements"
 * ```
 *
 * @module config/severity-overrides
 */
import { readFile, access } from "fs/promises";
import { join } from "path";
import { z } from "zod";
import { logger } from "../logger.js";
/**
 * Single severity override rule
 */
export const SeverityOverrideRuleSchema = z.object({
    /** Rule ID to match (exact or glob pattern) */
    ruleId: z.string().optional(),
    /** Scanner type to match */
    scanner: z.enum(["semgrep", "npm-audit", "gitleaks", "tsc", "eslint", "bandit", "gosec", "brakeman", "trivy"]).optional(),
    /** CWE ID to match */
    cwe: z.string().optional(),
    /** CVE ID to match */
    cve: z.string().optional(),
    /** File path pattern to match (glob) */
    file: z.string().optional(),
    /** New severity to apply */
    severity: z.enum(["critical", "high", "medium", "low", "info"]),
    /** Reason for override (documentation) */
    reason: z.string().optional(),
    /** Whether this override is active */
    enabled: z.boolean().default(true),
    /** Override expiration date (ISO 8601) */
    expiresAt: z.string().optional(),
});
/**
 * Severity overrides configuration file
 */
export const SeverityOverridesConfigSchema = z.object({
    /** Schema version */
    version: z.literal(1).default(1),
    /** Override rules */
    overrides: z.array(SeverityOverrideRuleSchema).default([]),
    /** Global settings */
    settings: z.object({
        /** Whether to log when overrides are applied */
        logOverrides: z.boolean().default(true),
        /** Whether to include override reason in finding metadata */
        includeReason: z.boolean().default(true),
    }).default({}),
});
/**
 * Load severity overrides configuration
 */
export async function loadSeverityOverrides(projectPath) {
    const configPaths = [
        join(projectPath, ".vaspera", "severity-overrides.yaml"),
        join(projectPath, ".vaspera", "severity-overrides.yml"),
        join(projectPath, ".vaspera", "severity-overrides.json"),
    ];
    for (const configPath of configPaths) {
        try {
            await access(configPath);
            const content = await readFile(configPath, "utf-8");
            // Parse based on extension
            let data;
            if (configPath.endsWith(".json")) {
                data = JSON.parse(content);
            }
            else {
                // Simple YAML parsing for common cases
                data = parseSimpleYaml(content);
            }
            const result = SeverityOverridesConfigSchema.safeParse(data);
            if (result.success) {
                logger.info("severity_overrides.loaded", {
                    path: configPath,
                    overrideCount: result.data.overrides.length,
                });
                return result.data;
            }
            else {
                logger.warn("severity_overrides.invalid_config", {
                    path: configPath,
                    errors: result.error.errors.map((e) => e.message),
                });
            }
        }
        catch {
            // File doesn't exist or can't be read
        }
    }
    // Return default empty config
    return {
        version: 1,
        overrides: [],
        settings: {
            logOverrides: true,
            includeReason: true,
        },
    };
}
/**
 * Simple YAML parser for common configuration structures
 */
function parseSimpleYaml(content) {
    const lines = content.split("\n");
    const result = {};
    let currentKey = "";
    let currentArray = null;
    let currentObject = null;
    let indentLevel = 0;
    for (const line of lines) {
        const trimmed = line.trim();
        // Skip comments and empty lines
        if (trimmed.startsWith("#") || trimmed === "")
            continue;
        const match = line.match(/^(\s*)(.+)$/);
        if (!match)
            continue;
        const indent = match[1].length;
        const content = match[2];
        // Top-level key
        if (indent === 0 && content.endsWith(":")) {
            currentKey = content.slice(0, -1);
            if (currentKey === "overrides") {
                currentArray = [];
                result[currentKey] = currentArray;
            }
            else if (currentKey === "settings") {
                currentObject = {};
                result[currentKey] = currentObject;
            }
            continue;
        }
        // Array item
        if (content.startsWith("- ") && currentArray) {
            const item = {};
            const firstProp = content.slice(2);
            if (firstProp.includes(": ")) {
                const [key, ...valueParts] = firstProp.split(": ");
                item[key] = parseYamlValue(valueParts.join(": "));
            }
            currentArray.push(item);
            currentObject = item;
            continue;
        }
        // Object property
        if (content.includes(": ") && currentObject) {
            const [key, ...valueParts] = content.split(": ");
            currentObject[key.trim()] = parseYamlValue(valueParts.join(": "));
        }
    }
    return result;
}
/**
 * Parse a YAML value
 */
function parseYamlValue(value) {
    const trimmed = value.trim();
    // Remove quotes
    if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
        (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
        return trimmed.slice(1, -1);
    }
    // Boolean
    if (trimmed === "true")
        return true;
    if (trimmed === "false")
        return false;
    // Number
    const num = Number(trimmed);
    if (!isNaN(num))
        return num;
    // String
    return trimmed;
}
/**
 * Check if a rule matches a finding
 */
function ruleMatchesFinding(rule, finding) {
    // Check if rule is enabled and not expired
    if (!rule.enabled) {
        return { matches: false };
    }
    if (rule.expiresAt) {
        const expiresAt = new Date(rule.expiresAt);
        if (expiresAt < new Date()) {
            return { matches: false };
        }
    }
    // Check scanner
    if (rule.scanner && rule.scanner !== finding.scanner) {
        return { matches: false };
    }
    // Check rule ID
    if (rule.ruleId) {
        const pattern = rule.ruleId.includes("*")
            ? new RegExp("^" + rule.ruleId.replace(/\*/g, ".*") + "$")
            : null;
        if (pattern) {
            if (!pattern.test(finding.ruleId)) {
                return { matches: false };
            }
        }
        else if (rule.ruleId !== finding.ruleId) {
            return { matches: false };
        }
    }
    // Check CWE
    if (rule.cwe) {
        if (!finding.cweIds?.includes(rule.cwe)) {
            return { matches: false };
        }
    }
    // Check CVE
    if (rule.cve) {
        if (!finding.cveIds?.includes(rule.cve)) {
            return { matches: false };
        }
    }
    // Check file pattern
    if (rule.file) {
        const pattern = new RegExp("^" + rule.file.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
        if (!pattern.test(finding.file)) {
            return { matches: false };
        }
    }
    // Build match reason
    const reasons = [];
    if (rule.ruleId)
        reasons.push(`ruleId=${rule.ruleId}`);
    if (rule.scanner)
        reasons.push(`scanner=${rule.scanner}`);
    if (rule.cwe)
        reasons.push(`cwe=${rule.cwe}`);
    if (rule.cve)
        reasons.push(`cve=${rule.cve}`);
    if (rule.file)
        reasons.push(`file=${rule.file}`);
    return {
        matches: true,
        reason: reasons.join(", "),
    };
}
/**
 * Apply severity overrides to a finding
 */
export function applySeverityOverride(finding, config) {
    for (const rule of config.overrides) {
        const match = ruleMatchesFinding(rule, finding);
        if (match.matches) {
            const override = {
                originalSeverity: finding.severity,
                newSeverity: rule.severity,
                rule,
                matchReason: match.reason || "matched",
            };
            // Create new finding with overridden severity
            const overriddenFinding = {
                ...finding,
                severity: rule.severity,
                metadata: {
                    ...finding.metadata,
                    severityOverride: {
                        original: finding.severity,
                        reason: rule.reason,
                        rule: rule.ruleId || rule.scanner || rule.file,
                    },
                },
            };
            if (config.settings.logOverrides) {
                logger.debug("severity_overrides.applied", {
                    file: finding.file,
                    line: finding.line,
                    ruleId: finding.ruleId,
                    originalSeverity: finding.severity,
                    newSeverity: rule.severity,
                    matchReason: match.reason,
                });
            }
            return { finding: overriddenFinding, override };
        }
    }
    return { finding };
}
/**
 * Apply severity overrides to multiple findings
 */
export function applySeverityOverrides(findings, config) {
    const results = [];
    const overrides = [];
    const byOriginalSeverity = {
        critical: 0,
        high: 0,
        medium: 0,
        low: 0,
        info: 0,
    };
    const byNewSeverity = {
        critical: 0,
        high: 0,
        medium: 0,
        low: 0,
        info: 0,
    };
    for (const finding of findings) {
        const result = applySeverityOverride(finding, config);
        results.push(result.finding);
        if (result.override) {
            overrides.push(result.override);
            byOriginalSeverity[result.override.originalSeverity]++;
            byNewSeverity[result.override.newSeverity]++;
        }
    }
    return {
        findings: results,
        overrides,
        summary: {
            total: findings.length,
            modified: overrides.length,
            byOriginalSeverity,
            byNewSeverity,
        },
    };
}
/**
 * Create a sample severity overrides configuration file
 */
export function generateSampleConfig() {
    return `# Vaspera Severity Overrides Configuration
# Place this file at .vaspera/severity-overrides.yaml

version: 1

settings:
  logOverrides: true
  includeReason: true

overrides:
  # Override by rule ID
  - ruleId: "semgrep:javascript.lang.security.audit.detect-non-literal-require"
    severity: "low"
    reason: "False positive - require paths are validated"

  # Override by scanner and CWE
  - scanner: "npm-audit"
    cwe: "CWE-1321"
    severity: "medium"
    reason: "Prototype pollution not exploitable in our context"

  # Override by file pattern
  - file: "tests/**"
    severity: "info"
    reason: "Test files have relaxed security requirements"

  # Override with expiration
  - ruleId: "gitleaks:generic-api-key"
    severity: "low"
    reason: "Temporary override while migrating to env vars"
    expiresAt: "2024-12-31T23:59:59Z"
`;
}
//# sourceMappingURL=severity-overrides.js.map