/**
 * secret-leak-detector.ts
 *
 * Detects common secret / credential patterns in outbound HTTP request
 * bodies, URLs, and header values.  Used by RawHttpInterceptor to prevent
 * accidental exfiltration of API keys, tokens, or other high-entropy secrets.
 *
 * Pattern catalog (v1):
 *   - OpenAI API keys      sk-...
 *   - Anthropic API keys   sk-ant-...
 *   - Slack tokens         xoxb-..., xoxp-..., xoxa-..., xoxs-...
 *   - GitHub tokens        ghp_..., gho_..., ghs_..., ghr_...
 *   - AWS Access keys      AKIA..., ASIA...
 *   - Generic Bearer token in Authorization header
 *   - Long base64 payloads >= 60 chars (high-entropy bearer tokens, JWTs)
 *   - Long hex strings     >= 40 chars (hashed secrets, signing keys)
 *
 * Each pattern produces a LeakFinding with a sanitized preview (never the
 * raw secret value).
 *
 * @connexum/ai-governance
 */
const PATTERNS = [
    {
        id: 'SLD-001',
        category: 'openai-key',
        label: 'OpenAI API key (sk-...)',
        pattern: /sk-[A-Za-z0-9]{20,}/g,
    },
    {
        id: 'SLD-002',
        category: 'anthropic-key',
        label: 'Anthropic API key (sk-ant-...)',
        pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g,
    },
    {
        id: 'SLD-003',
        category: 'slack-token',
        label: 'Slack token (xox[bpas]-...)',
        pattern: /xox[bpas]-[A-Za-z0-9-]{10,}/g,
    },
    {
        id: 'SLD-004',
        category: 'github-token',
        label: 'GitHub token (ghp_/gho_/ghs_/ghr_...)',
        pattern: /gh[psohr]_[A-Za-z0-9]{36,}/g,
    },
    {
        id: 'SLD-005',
        category: 'aws-access-key',
        label: 'AWS Access Key ID (AKIA.../ASIA...)',
        pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
    },
    {
        id: 'SLD-006',
        category: 'generic-bearer',
        label: 'Generic Bearer token in text',
        // Matches "Bearer <token>" where token is >= 20 chars
        pattern: /Bearer\s+[A-Za-z0-9+/=_.-]{20,}/g,
    },
    {
        id: 'SLD-007',
        category: 'high-entropy-base64',
        label: 'High-entropy base64 payload (>= 60 chars)',
        // Continuous base64 alphabet run >= 60 chars (potential JWT or token)
        pattern: /[A-Za-z0-9+/]{60,}={0,2}/g,
    },
    {
        id: 'SLD-008',
        category: 'high-entropy-hex',
        label: 'High-entropy hex string (>= 40 chars)',
        pattern: /\b[0-9a-f]{40,}\b/gi,
    },
];
// ---------------------------------------------------------------------------
// Helper -- build redacted preview
// ---------------------------------------------------------------------------
function redactedPreview(match) {
    // Show first 4 chars and last 2 chars; mask the middle
    if (match.length <= 8)
        return '***';
    const head = match.slice(0, 4);
    const tail = match.slice(-2);
    const masked = '*'.repeat(Math.min(match.length - 6, 12));
    return `${head}${masked}${tail}`;
}
// ---------------------------------------------------------------------------
// SecretLeakDetector
// ---------------------------------------------------------------------------
export class SecretLeakDetector {
    /**
     * Scan a text string (body, URL, or header value) for secret patterns.
     *
     * @param text    The text to scan.
     * @param surface Which surface this text came from (for audit tagging).
     */
    scan(text, surface) {
        const findings = [];
        for (const pat of PATTERNS) {
            // Reset lastIndex for stateful global regex
            const regex = new RegExp(pat.pattern.source, pat.pattern.flags);
            let m;
            while ((m = regex.exec(text)) !== null) {
                findings.push({
                    patternId: pat.id,
                    category: pat.category,
                    label: pat.label,
                    surface,
                    redactedPreview: redactedPreview(m[0]),
                });
                // Advance to avoid infinite loops on zero-width matches
                if (m[0].length === 0)
                    regex.lastIndex++;
            }
        }
        // Deduplicate by patternId + surface (keep first occurrence only)
        const seen = new Set();
        const deduplicated = [];
        for (const f of findings) {
            const key = `${f.patternId}:${f.surface}`;
            if (!seen.has(key)) {
                seen.add(key);
                deduplicated.push(f);
            }
        }
        return {
            detected: deduplicated.length > 0,
            findings: deduplicated,
        };
    }
    /**
     * Scan multiple surfaces in one call.
     *
     * @param body    Raw request body as string (pass '' if none).
     * @param urlStr  Full URL string (query string included for detection).
     * @param headers Record of header name -> value.
     */
    scanAll(body, urlStr, headers) {
        const results = [];
        if (body) {
            results.push(...this.scan(body, 'body').findings);
        }
        if (urlStr) {
            // Scan the full URL including query string
            results.push(...this.scan(urlStr, 'url').findings);
        }
        // Scan sensitive header values -- skip Authorization/X-API-Key in audit
        // (we detect presence but never log the raw value)
        for (const [name, value] of Object.entries(headers)) {
            const nameLower = name.toLowerCase();
            // Skip known auth headers from body scan -- they are handled by the
            // interceptor's header-redaction layer separately.
            if (nameLower === 'authorization' ||
                nameLower === 'x-api-key' ||
                nameLower === 'x-goog-api-key') {
                // Still check for leaks in the value but tag surface as 'header'
                results.push(...this.scan(value, 'header').findings);
            }
        }
        // Deduplicate across all surfaces by patternId + surface
        const seen = new Set();
        const deduplicated = [];
        for (const f of results) {
            const key = `${f.patternId}:${f.surface}`;
            if (!seen.has(key)) {
                seen.add(key);
                deduplicated.push(f);
            }
        }
        return {
            detected: deduplicated.length > 0,
            findings: deduplicated,
        };
    }
}
//# sourceMappingURL=secret-leak-detector.js.map