// ─── Secret Detection & Redaction Engine ──────────────────────────────────────
// OMK Protocol Gateway — P2.6: Secret Scanner
//
// Standalone secret detection engine with:
//   - Severity levels (CRITICAL, HIGH, MEDIUM, LOW)
//   - Multiple scanning modes (QUICK, DEEP, PARANOID)
//   - File and directory scanning
//   - Structured reports with statistics
//   - Custom pattern management
//   - Integration with governance redaction pipeline
//
// Re-exports and extends the patterns from governance.ts redactSecrets().
import { readFile, readdir, lstat } from "node:fs/promises";
import { join, extname } from "node:path";
import { EventEmitter } from "node:events";
import { SECRET_KEY_NAMES } from "./shared-secret-registry.js";
// ─── Types ───────────────────────────────────────────────────────────────────
/** Severity of a detected secret */
export var SecretSeverity;
(function (SecretSeverity) {
    /** Confirmed credential — immediate risk (AWS keys, tokens, passwords) */
    SecretSeverity["CRITICAL"] = "critical";
    /** High-confidence secret pattern (API keys, JWTs, private keys) */
    SecretSeverity["HIGH"] = "high";
    /** Possible secret (generic env vars, hex strings) */
    SecretSeverity["MEDIUM"] = "medium";
    /** Low-confidence / informational (short tokens, partial matches) */
    SecretSeverity["LOW"] = "low";
})(SecretSeverity || (SecretSeverity = {}));
/** Scanning mode controls depth vs speed tradeoff */
export var ScanMode;
(function (ScanMode) {
    /** Fast — common patterns only, no file I/O */
    ScanMode["QUICK"] = "quick";
    /** Balanced — all patterns, file scanning, context analysis */
    ScanMode["DEEP"] = "deep";
    /** Thorough — all patterns, entropy analysis, aggressive heuristics */
    ScanMode["PARANOID"] = "paranoid";
})(ScanMode || (ScanMode = {}));
// ─── Built-in Secret Patterns ────────────────────────────────────────────────
const BUILTIN_PATTERNS = [
    // CRITICAL — confirmed credentials
    {
        name: "aws_access_key",
        description: "AWS Access Key ID",
        pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
        replacement: "[REDACTED_AWS_KEY]",
        severity: SecretSeverity.CRITICAL,
        category: "cloud",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "aws_secret_key",
        description: "AWS Secret Access Key",
        pattern: /(?:aws_secret_access_key|aws_secret)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})["']?/gi,
        replacement: "[REDACTED_AWS_SECRET]",
        severity: SecretSeverity.CRITICAL,
        category: "cloud",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "private_key",
        description: "PEM Private Key",
        pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
        replacement: "[REDACTED_PRIVATE_KEY]",
        severity: SecretSeverity.CRITICAL,
        category: "crypto",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "github_token",
        description: "GitHub Personal Access Token",
        pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g,
        replacement: "[REDACTED_GITHUB_TOKEN]",
        severity: SecretSeverity.CRITICAL,
        category: "scm",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "gitlab_token",
        description: "GitLab Personal Access Token",
        pattern: /glpat-[A-Za-z0-9_-]{20,}/g,
        replacement: "[REDACTED_GITLAB_TOKEN]",
        severity: SecretSeverity.CRITICAL,
        category: "scm",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "slack_token",
        description: "Slack Bot/User/Workspace Token",
        pattern: /xox[bpoas]-[A-Za-z0-9-]{10,}/g,
        replacement: "[REDACTED_SLACK_TOKEN]",
        severity: SecretSeverity.CRITICAL,
        category: "messaging",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    // HIGH — strong patterns for API keys and tokens
    {
        name: "openai_key",
        description: "OpenAI API Key",
        pattern: /sk-[A-Za-z0-9]{20,}/g,
        replacement: "[REDACTED_OPENAI_KEY]",
        severity: SecretSeverity.HIGH,
        category: "ai",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "openai_project_key",
        description: "OpenAI Project API Key",
        pattern: /\bsk-proj-[A-Za-z0-9_-]{16,}\b/g,
        replacement: "[REDACTED_OPENAI_PROJECT_KEY]",
        severity: SecretSeverity.HIGH,
        category: "ai",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "openai_service_account_key",
        description: "OpenAI Service Account API Key",
        pattern: /\bsk-svcacct-[A-Za-z0-9_-]{16,}\b/g,
        replacement: "[REDACTED_OPENAI_SERVICE_KEY]",
        severity: SecretSeverity.HIGH,
        category: "ai",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "oauth_session_token",
        description: "OAuth or Session Token",
        pattern: /\b(?:oauth|session|refresh|access)[_-]?token\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{20,}\b/gi,
        replacement: "[REDACTED_OAUTH_TOKEN]",
        severity: SecretSeverity.HIGH,
        category: "auth",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "anthropic_key",
        description: "Anthropic API Key",
        pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g,
        replacement: "[REDACTED_ANTHROPIC_KEY]",
        severity: SecretSeverity.HIGH,
        category: "ai",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "google_api_key",
        description: "Google API Key",
        pattern: /AIza[A-Za-z0-9_-]{35}/g,
        replacement: "[REDACTED_GOOGLE_KEY]",
        severity: SecretSeverity.HIGH,
        category: "cloud",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "jwt_token",
        description: "JSON Web Token",
        pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
        replacement: "[REDACTED_JWT]",
        severity: SecretSeverity.HIGH,
        category: "auth",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "bearer_token",
        description: "Bearer Authorization Token",
        pattern: /Bearer\s+[A-Za-z0-9_.-]{20,}/gi,
        replacement: "Bearer [REDACTED_TOKEN]",
        severity: SecretSeverity.HIGH,
        category: "auth",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "api_key_generic",
        description: "Generic API Key assignment",
        pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']?([A-Za-z0-9_-]{20,})["']?/gi,
        replacement: "[REDACTED_API_KEY]",
        severity: SecretSeverity.HIGH,
        category: "auth",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    {
        name: "connection_string",
        description: "Database Connection String with Password",
        pattern: /(?:mongodb|postgres|mysql|redis|mssql):\/\/[^:]+:([^@]+)@/gi,
        replacement: "://[REDACTED]:[REDACTED]@",
        severity: SecretSeverity.HIGH,
        category: "database",
        enabled: true,
        minMode: ScanMode.QUICK,
    },
    // MEDIUM — generic env-style secrets
    {
        name: "secret_env_var",
        description: "Secret Environment Variable Assignment",
        pattern: /(?:SECRET|TOKEN|PASSWORD|PASSWD|PWD|CREDENTIAL|AUTH)\s*[:=]\s*["']?([^\s"']{8,})["']?/gi,
        replacement: "[REDACTED_SECRET]",
        severity: SecretSeverity.MEDIUM,
        category: "env",
        enabled: true,
        minMode: ScanMode.DEEP,
    },
    {
        name: "hex_secret",
        description: "Hex-encoded Secret",
        pattern: /(?:secret|key|token|password)\s*[:=]\s*["']?([a-f0-9]{32,})["']?/gi,
        replacement: "[REDACTED_HEX_SECRET]",
        severity: SecretSeverity.MEDIUM,
        category: "env",
        enabled: true,
        minMode: ScanMode.DEEP,
    },
    // LOW — paranoid-mode patterns
    {
        name: "base64_long",
        description: "Long Base64 String (potential encoded secret)",
        pattern: /["']?[A-Za-z0-9+/]{60,}={0,2}["']?/g,
        replacement: "[REDACTED_BASE64]",
        severity: SecretSeverity.LOW,
        category: "encoded",
        enabled: true,
        minMode: ScanMode.PARANOID,
    },
    {
        name: "hex_long",
        description: "Long Hex String (potential encoded secret)",
        pattern: /["']?[a-f0-9]{64,}["']?/gi,
        replacement: "[REDACTED_HEX]",
        severity: SecretSeverity.LOW,
        category: "encoded",
        enabled: true,
        minMode: ScanMode.PARANOID,
    },
];
// ─── Key Name Patterns (for arg sanitization) ───────────────────────────────
/** Known secret key names for arg-level redaction */
// Re-exported from shared-secret-registry.ts (P2.7.6: eliminate duplication)
export { SECRET_KEY_NAMES };
// ─── Scanner Class ───────────────────────────────────────────────────────────
export class SecretScanner extends EventEmitter {
    patterns;
    mode;
    maxFileSize;
    includeExtensions;
    excludeExtensions;
    excludeDirs;
    contextChars;
    sourceLabel;
    static DEFAULT_TEXT_EXTENSIONS = new Set([
        ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
        ".json", ".yaml", ".yml", ".toml",
        ".env", ".env.local", ".env.production", ".env.development",
        ".md", ".txt", ".csv", ".log",
        ".sh", ".bash", ".zsh", ".fish",
        ".py", ".rb", ".go", ".rs", ".java", ".kt",
        ".xml", ".html", ".css", ".scss",
        ".cfg", ".conf", ".ini", ".properties",
        ".pem", ".key", ".crt", ".cer",
    ]);
    static DEFAULT_EXCLUDE_DIRS = new Set([
        "node_modules", ".git", "dist", "build", ".next", "__pycache__",
        ".cache", ".venv", "vendor", "coverage", ".turbo",
    ]);
    static BINARY_EXTENSIONS = new Set([
        ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".svg",
        ".woff", ".woff2", ".ttf", ".eot", ".otf",
        ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
        ".mp3", ".mp4", ".avi", ".mov", ".wav",
        ".exe", ".dll", ".so", ".dylib", ".bin",
        ".pdf", ".doc", ".docx", ".xls", ".xlsx",
        ".sqlite", ".db",
    ]);
    constructor(options) {
        super();
        this.mode = options?.mode ?? ScanMode.DEEP;
        this.maxFileSize = options?.maxFileSize ?? 1024 * 1024; // 1MB
        this.includeExtensions = options?.includeExtensions
            ? new Set(options.includeExtensions)
            : SecretScanner.DEFAULT_TEXT_EXTENSIONS;
        this.excludeExtensions = new Set([
            ...SecretScanner.BINARY_EXTENSIONS,
            ...(options?.excludeExtensions ?? []),
        ]);
        this.excludeDirs = new Set([
            ...SecretScanner.DEFAULT_EXCLUDE_DIRS,
            ...(options?.excludeDirs ?? []),
        ]);
        this.contextChars = options?.contextChars ?? 40;
        this.sourceLabel = options?.sourceLabel ?? "input";
        // Build active pattern list
        const disabled = new Set(options?.disabledPatterns ?? []);
        this.patterns = [
            ...BUILTIN_PATTERNS.filter((p) => !disabled.has(p.name)),
            ...(options?.customPatterns ?? []),
        ];
    }
    // ── Text Scanning ──
    /** Scan text for secrets. Returns report with findings and redacted text. */
    scanText(text, sourceLabel) {
        const start = Date.now();
        const findings = [];
        let redacted = text;
        const activePatterns = this.getActivePatterns();
        for (const pattern of activePatterns) {
            pattern.pattern.lastIndex = 0;
            let match;
            while ((match = pattern.pattern.exec(text)) !== null) {
                const matchedText = match[0];
                const offset = match.index;
                const { line, column } = this.getLineColumn(text, offset);
                const context = this.getContext(text, offset, matchedText.length);
                findings.push({
                    id: this.generateId(),
                    patternName: pattern.name,
                    severity: pattern.severity,
                    category: pattern.category,
                    redactedMatch: pattern.replacement,
                    originalLength: matchedText.length,
                    offset,
                    line,
                    column,
                    sourceFile: sourceLabel ?? this.sourceLabel,
                    context,
                });
                this.emit("finding", findings[findings.length - 1]);
            }
            // Apply redaction
            pattern.pattern.lastIndex = 0;
            redacted = redacted.replace(pattern.pattern, pattern.replacement);
        }
        const durationMs = Date.now() - start;
        const stats = this.buildStats(findings, activePatterns.length, text.length, 0, 0, durationMs);
        const report = this.buildReport(findings, stats, redacted);
        this.emit("scan:complete", report);
        return report;
    }
    // ── File Scanning ──
    /** Scan a single file for secrets. */
    async scanFile(filePath) {
        const start = Date.now();
        const content = await this.readFileSafe(filePath);
        if (content === null) {
            // Binary or unreadable — return empty report
            const durationMs = Date.now() - start;
            return this.buildReport([], this.buildStats([], 0, 0, 0, 1, durationMs, ScanMode.QUICK));
        }
        const findings = this.findInText(content, filePath);
        const redacted = this.redactText(content);
        const durationMs = Date.now() - start;
        const stats = this.buildStats(findings, this.getActivePatterns().length, content.length, 1, 0, durationMs);
        const report = this.buildReport(findings, stats, redacted);
        this.emit("scan:complete", report);
        return report;
    }
    /** Scan a directory recursively for secrets. */
    async scanDir(dirPath) {
        const start = Date.now();
        const allFindings = [];
        let totalChars = 0;
        let filesScanned = 0;
        let filesSkipped = 0;
        const files = await this.walkDir(dirPath);
        const activePatterns = this.getActivePatterns();
        for (const filePath of files) {
            const content = await this.readFileSafe(filePath);
            if (content === null) {
                filesSkipped++;
                continue;
            }
            filesScanned++;
            totalChars += content.length;
            const findings = this.findInText(content, filePath);
            allFindings.push(...findings);
            this.emit("file:scanned", { path: filePath, findings: findings.length });
        }
        const durationMs = Date.now() - start;
        const stats = this.buildStats(allFindings, activePatterns.length, totalChars, filesScanned, filesSkipped, durationMs);
        const report = this.buildReport(allFindings, stats);
        this.emit("scan:complete", report);
        return report;
    }
    // ── Arg Sanitization ──
    /** Sanitize tool arguments, redacting known secret key names. */
    sanitizeArgs(args) {
        const sanitized = {};
        for (const [key, value] of Object.entries(args)) {
            const lowerKey = key.toLowerCase();
            const normalized = lowerKey.replace(/[_-]/g, "");
            if (SECRET_KEY_NAMES.has(lowerKey) || SECRET_KEY_NAMES.has(normalized)) {
                sanitized[key] = "[REDACTED]";
            }
            else if (typeof value === "string") {
                sanitized[key] = this.redactText(value);
            }
            else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
                sanitized[key] = this.sanitizeArgs(value);
            }
            else {
                sanitized[key] = value;
            }
        }
        return sanitized;
    }
    // ── Pattern Management ──
    /** Get all active patterns for current mode. */
    getActivePatterns() {
        const modeOrder = ScanMode.QUICK === this.mode ? 0 : this.mode === ScanMode.DEEP ? 1 : 2;
        return this.patterns.filter((p) => {
            if (!p.enabled)
                return false;
            const pMode = p.minMode === ScanMode.QUICK ? 0 : p.minMode === ScanMode.DEEP ? 1 : 2;
            return pMode <= modeOrder;
        });
    }
    /** Add a custom pattern at runtime. */
    addPattern(pattern) {
        this.patterns.push(pattern);
    }
    /** Remove a pattern by name. */
    removePattern(name) {
        const idx = this.patterns.findIndex((p) => p.name === name);
        if (idx >= 0) {
            this.patterns.splice(idx, 1);
            return true;
        }
        return false;
    }
    /** Enable or disable a pattern. */
    setPatternEnabled(name, enabled) {
        const pattern = this.patterns.find((p) => p.name === name);
        if (pattern) {
            pattern.enabled = enabled;
            return true;
        }
        return false;
    }
    /** List all registered patterns. */
    listPatterns() {
        return [...this.patterns];
    }
    // ── Report Formatting ──
    /** Format a scan report for terminal display. */
    static formatReport(report) {
        const lines = [];
        const { findings, stats, mode } = report;
        // Header
        const severityIcon = report.hasCriticalFindings ? "🚨" : findings.length > 0 ? "⚠️" : "✅";
        lines.push("");
        lines.push(`  ${severityIcon} Secret Scan Report [${mode.toUpperCase()}]`);
        lines.push("  " + "─".repeat(50));
        if (findings.length === 0) {
            lines.push("  No secrets detected.");
        }
        else {
            // Group by severity
            const grouped = new Map();
            for (const f of findings) {
                if (!grouped.has(f.severity))
                    grouped.set(f.severity, []);
                grouped.get(f.severity).push(f);
            }
            for (const severity of [SecretSeverity.CRITICAL, SecretSeverity.HIGH, SecretSeverity.MEDIUM, SecretSeverity.LOW]) {
                const group = grouped.get(severity);
                if (!group || group.length === 0)
                    continue;
                const icon = SecretScanner.severityIcon(severity);
                lines.push("");
                lines.push(`  ${icon} ${severity.toUpperCase()} (${group.length})`);
                for (const f of group.slice(0, 10)) {
                    const loc = f.sourceFile ? `${f.sourceFile}:${f.line}:${f.column}` : `line ${f.line}:${f.column}`;
                    lines.push(`    ├─ ${f.patternName} @ ${loc}`);
                    lines.push(`    │  ${f.context}`);
                }
                if (group.length > 10) {
                    lines.push(`    └─ ... and ${group.length - 10} more`);
                }
            }
        }
        // Stats
        lines.push("");
        lines.push("  " + "─".repeat(50));
        lines.push(`  Findings: ${stats.totalFindings} | Critical: ${stats.bySeverity[SecretSeverity.CRITICAL] ?? 0} | High: ${stats.bySeverity[SecretSeverity.HIGH] ?? 0}`);
        lines.push(`  Files: ${stats.filesScanned} scanned, ${stats.filesSkipped} skipped | ${stats.charsScanned.toLocaleString()} chars | ${stats.durationMs}ms`);
        lines.push("");
        return lines.join("\n");
    }
    /** Format a compact summary line. */
    static formatSummary(report) {
        const { stats, findings } = report;
        if (findings.length === 0)
            return "✅ No secrets detected";
        const critical = stats.bySeverity[SecretSeverity.CRITICAL] ?? 0;
        const high = stats.bySeverity[SecretSeverity.HIGH] ?? 0;
        const icon = critical > 0 ? "🚨" : "⚠️";
        return `${icon} ${findings.length} secret(s) found (${critical} critical, ${high} high)`;
    }
    /** Get severity icon for terminal display. */
    static severityIcon(severity) {
        switch (severity) {
            case SecretSeverity.CRITICAL: return "🔴";
            case SecretSeverity.HIGH: return "🟠";
            case SecretSeverity.MEDIUM: return "🟡";
            case SecretSeverity.LOW: return "⚪";
        }
    }
    // ── Internal Helpers ──
    findInText(text, sourceFile) {
        const findings = [];
        const activePatterns = this.getActivePatterns();
        for (const pattern of activePatterns) {
            pattern.pattern.lastIndex = 0;
            let match;
            while ((match = pattern.pattern.exec(text)) !== null) {
                const matchedText = match[0];
                const offset = match.index;
                const { line, column } = this.getLineColumn(text, offset);
                const context = this.getContext(text, offset, matchedText.length);
                findings.push({
                    id: this.generateId(),
                    patternName: pattern.name,
                    severity: pattern.severity,
                    category: pattern.category,
                    redactedMatch: pattern.replacement,
                    originalLength: matchedText.length,
                    offset,
                    line,
                    column,
                    sourceFile,
                    context,
                });
                this.emit("finding", findings[findings.length - 1]);
            }
        }
        return findings;
    }
    redactText(text) {
        let result = text;
        for (const pattern of this.getActivePatterns()) {
            pattern.pattern.lastIndex = 0;
            result = result.replace(pattern.pattern, pattern.replacement);
        }
        return result;
    }
    getLineColumn(text, offset) {
        let line = 1;
        let column = 1;
        for (let i = 0; i < offset && i < text.length; i++) {
            if (text[i] === "\n") {
                line++;
                column = 1;
            }
            else {
                column++;
            }
        }
        return { line, column };
    }
    getContext(text, offset, matchLength) {
        const ctxChars = this.contextChars;
        const start = Math.max(0, offset - ctxChars);
        const end = Math.min(text.length, offset + matchLength + ctxChars);
        let snippet = "";
        if (start > 0)
            snippet += "...";
        snippet += text.slice(start, offset);
        snippet += "█".repeat(Math.min(matchLength, 20));
        snippet += text.slice(offset + matchLength, end);
        if (end < text.length)
            snippet += "...";
        return snippet.replace(/\n/g, " ").slice(0, 120);
    }
    async readFileSafe(filePath) {
        try {
            const fileStat = await lstat(filePath);
            if (fileStat.isSymbolicLink())
                return null;
            if (fileStat.size > this.maxFileSize)
                return null;
            const ext = extname(filePath).toLowerCase();
            const effectiveExt = ext || (filePath.split(/[/\\]/).pop()?.startsWith(".") ? filePath.split(/[/\\]/).pop().toLowerCase() : "");
            if (this.excludeExtensions.has(effectiveExt))
                return null;
            const content = await readFile(filePath, "utf-8");
            // Quick binary check: if content has null bytes, skip
            if (content.includes("\0"))
                return null;
            return content;
        }
        catch {
            return null;
        }
    }
    async walkDir(dirPath) {
        const results = [];
        const walk = async (current) => {
            let entries;
            try {
                entries = await readdir(current, { withFileTypes: true });
            }
            catch {
                return;
            }
            for (const entry of entries) {
                const fullPath = join(current, entry.name);
                if (entry.isDirectory()) {
                    if (!this.excludeDirs.has(entry.name)) {
                        await walk(fullPath);
                    }
                }
                else if (entry.isFile()) {
                    const ext = extname(entry.name).toLowerCase();
                    // For dotfiles like .env, extname returns "" — check full name
                    const effectiveExt = ext || (entry.name.startsWith(".") ? entry.name.toLowerCase() : "");
                    if (!this.excludeExtensions.has(effectiveExt) && this.includeExtensions.has(effectiveExt)) {
                        results.push(fullPath);
                    }
                }
            }
        };
        await walk(dirPath);
        return results;
    }
    buildStats(findings, patternsChecked, charsScanned, filesScanned, filesSkipped, durationMs, mode) {
        const bySeverity = {};
        const byCategory = {};
        for (const severity of Object.values(SecretSeverity)) {
            bySeverity[severity] = 0;
        }
        for (const f of findings) {
            bySeverity[f.severity]++;
            byCategory[f.category] = (byCategory[f.category] ?? 0) + 1;
        }
        return {
            patternsChecked,
            patternsMatched: new Set(findings.map((f) => f.patternName)).size,
            totalFindings: findings.length,
            bySeverity,
            byCategory,
            charsScanned,
            filesScanned,
            filesSkipped,
            durationMs,
            mode: mode ?? this.mode,
        };
    }
    buildReport(findings, stats, redactedText) {
        const hasCritical = (stats.bySeverity[SecretSeverity.CRITICAL] ?? 0) > 0
            || (stats.bySeverity[SecretSeverity.HIGH] ?? 0) > 0;
        return {
            id: this.generateId(),
            timestamp: new Date().toISOString(),
            mode: this.mode,
            findings,
            redactedText,
            stats,
            hasCriticalFindings: hasCritical,
            summary: SecretScanner.formatSummary({ id: "", timestamp: "", mode: this.mode, findings, redactedText, stats, hasCriticalFindings: hasCritical, summary: "" }),
        };
    }
    generateId() {
        return `ss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
    }
}
// ─── Convenience Functions ────────────────────────────────────────────────────
/** Quick scan text with default settings. */
export function scanText(text, mode) {
    return new SecretScanner({ mode }).scanText(text);
}
/** Quick scan a file. */
export async function scanFile(filePath, mode) {
    return new SecretScanner({ mode }).scanFile(filePath);
}
/** Quick scan a directory. */
export async function scanDir(dirPath, mode) {
    return new SecretScanner({ mode }).scanDir(dirPath);
}
/** Redact secrets from text using governance-compatible patterns. */
export function redactSecrets(text, customPatterns) {
    const scanner = new SecretScanner({
        mode: ScanMode.DEEP,
        customPatterns: customPatterns?.map((pattern, index) => ({
            name: `custom_${index}`,
            description: "Custom redaction pattern",
            pattern,
            replacement: "[REDACTED_CUSTOM_SECRET]",
            severity: SecretSeverity.HIGH,
            category: "custom",
            enabled: true,
            minMode: ScanMode.QUICK,
        })),
    });
    const report = scanner.scanText(text);
    return {
        redacted: report.redactedText ?? text,
        count: report.stats.totalFindings,
    };
}
