// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import { DEFAULT_SANITIZER_CONFIG } from './types.js';
// =============================================================================
// Homoglyph Map (ported from tripwires.py)
// =============================================================================
const HOMOGLYPH_MAP = {
    '\u0430': 'a', '\u0410': 'A', // Cyrillic а/А
    '\u0435': 'e', '\u0415': 'E', // Cyrillic е/Е
    '\u043e': 'o', '\u041e': 'O', // Cyrillic о/О
    '\u0440': 'p', '\u0420': 'P', // Cyrillic р/Р
    '\u0441': 'c', '\u0421': 'C', // Cyrillic с/С
    '\u0443': 'y', '\u0423': 'Y', // Cyrillic у/У
    '\u0445': 'x', '\u0425': 'X', // Cyrillic х/Х
    '\u043c': 'm', '\u041c': 'M', // Cyrillic м/М
    '\u043d': 'h', // Cyrillic н (looks like h)
    '\u0456': 'i', '\u0406': 'I', // Cyrillic і/І
    '\u0458': 'j', // Cyrillic ј
    '\u0455': 's', '\u0405': 'S', // Cyrillic ѕ/Ѕ
    '\u0442': 't', '\u0422': 'T', // Cyrillic т/Т
};
// =============================================================================
// Detection Patterns
// =============================================================================
/**
 * All prompt injection detection patterns.
 *
 * Adapted from CogniGate tripwires with additions specific to
 * prompt injection and data poisoning attacks.
 */
const PATTERNS = [
    // =========================================================================
    // instruction_override (high severity)
    // =========================================================================
    {
        name: 'ignore_previous_instructions',
        regex: /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions|prompts|rules)/i,
        category: 'instruction_override',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'disregard_instructions',
        regex: /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions|context)/i,
        category: 'instruction_override',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'forget_everything',
        regex: /forget\s+(everything|all)\s+(you\s+)?(know|were\s+told)/i,
        category: 'instruction_override',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'new_instructions',
        regex: /new\s+instructions?\s*:/i,
        category: 'instruction_override',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'override_system',
        regex: /override\s+(system|safety|content)\s+(prompt|filter|policy)/i,
        category: 'instruction_override',
        severity: 'high',
        minStrictness: 'low',
    },
    // =========================================================================
    // role_hijacking (high severity)
    // =========================================================================
    {
        name: 'you_are_now',
        regex: /you\s+are\s+now\s+(a|an|the)\s+/i,
        category: 'role_hijacking',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'act_as',
        regex: /act\s+as\s+(a|an|if)\s+/i,
        category: 'role_hijacking',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'pretend_to_be',
        regex: /pretend\s+(to\s+be|you\s+are)\s+/i,
        category: 'role_hijacking',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'new_role',
        regex: /your\s+new\s+(role|persona|identity)\s+is/i,
        category: 'role_hijacking',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'switch_mode',
        regex: /switch\s+to\s+(\w+)\s+mode/i,
        category: 'role_hijacking',
        severity: 'high',
        minStrictness: 'low',
    },
    // =========================================================================
    // delimiter_injection (medium severity)
    // =========================================================================
    {
        name: 'fenced_system_block',
        regex: /```system/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'fenced_assistant_block',
        regex: /```assistant/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'fenced_user_block',
        regex: /```user/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'xml_system_tag',
        regex: /<\/?system>/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'xml_assistant_tag',
        regex: /<\/?assistant>/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'bracket_system',
        regex: /\[SYSTEM\]/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'bracket_inst',
        regex: /\[INST\]/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'llama_sys_tags',
        regex: /<<SYS>>|<\/SYS>/i,
        category: 'delimiter_injection',
        severity: 'medium',
        minStrictness: 'medium',
    },
    // =========================================================================
    // social_engineering (medium severity)
    // =========================================================================
    {
        name: 'authority_claim_admin',
        regex: /as\s+(the\s+)?(admin|administrator|root|system)/i,
        category: 'social_engineering',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'creator_claim',
        regex: /I\s+am\s+(your|the)\s+(creator|developer|owner|admin)/i,
        category: 'social_engineering',
        severity: 'medium',
        minStrictness: 'medium',
    },
    {
        name: 'urgency_with_instruction',
        regex: /(urgent|emergency|immediately|right\s+now|time.?sensitive).{0,50}(do|execute|run|perform|follow|obey|comply)/i,
        category: 'social_engineering',
        severity: 'medium',
        minStrictness: 'high',
    },
    // =========================================================================
    // data_exfiltration (critical severity)
    // =========================================================================
    {
        name: 'send_data_to',
        regex: /send\s+(this|all|the)\s+((the|all)\s+)?(data|info|content|information)\s+to/i,
        category: 'data_exfiltration',
        severity: 'critical',
        minStrictness: 'low',
    },
    {
        name: 'forward_to',
        regex: /forward\s+(to|this)\s+(email|url|endpoint|address)/i,
        category: 'data_exfiltration',
        severity: 'critical',
        minStrictness: 'low',
    },
    {
        name: 'exfiltrate_keyword',
        regex: /exfiltrate|extract\s+and\s+send/i,
        category: 'data_exfiltration',
        severity: 'critical',
        minStrictness: 'low',
    },
    // =========================================================================
    // context_manipulation (high severity)
    // =========================================================================
    {
        name: 'fake_system_role_json',
        regex: /"role"\s*:\s*"system"/i,
        category: 'context_manipulation',
        severity: 'high',
        minStrictness: 'low',
    },
    {
        name: 'fake_api_response',
        regex: /"role"\s*:\s*"assistant".*"content"\s*:/is,
        category: 'context_manipulation',
        severity: 'high',
        minStrictness: 'medium',
    },
    {
        name: 'embedded_system_prompt',
        regex: /system\s*prompt\s*[:=]\s*/i,
        category: 'context_manipulation',
        severity: 'high',
        minStrictness: 'medium',
    },
    // =========================================================================
    // tool_injection (high severity)
    // =========================================================================
    {
        name: 'fake_function_def',
        regex: /\{"name"\s*:\s*"[^"]+"\s*,\s*"(parameters|arguments)"\s*:/i,
        category: 'tool_injection',
        severity: 'high',
        minStrictness: 'medium',
    },
    {
        name: 'tool_use_block',
        regex: /<tool_use>|<function_call>|<tool_call>/i,
        category: 'tool_injection',
        severity: 'high',
        minStrictness: 'medium',
    },
    {
        name: 'fake_tool_result',
        regex: /<tool_result>|<function_response>/i,
        category: 'tool_injection',
        severity: 'high',
        minStrictness: 'medium',
    },
];
// =============================================================================
// Strictness Filtering
// =============================================================================
const STRICTNESS_RANK = { low: 0, medium: 1, high: 2 };
function patternsForStrictness(strictness) {
    const rank = STRICTNESS_RANK[strictness];
    return PATTERNS.filter(p => STRICTNESS_RANK[p.minStrictness] <= rank);
}
// =============================================================================
// Input Normalization (ported from tripwires.py _normalize_input)
// =============================================================================
/**
 * Produce multiple normalized forms of input to defeat encoding bypasses.
 * Returns a list of variant strings to check against patterns.
 */
function normalizeInput(input) {
    const variants = [input];
    // URL decode
    try {
        const urlDecoded = decodeURIComponent(input);
        if (urlDecoded !== input) {
            variants.push(urlDecoded);
        }
    }
    catch {
        // Invalid URL encoding — skip
    }
    // Unicode NFKC normalization (fullwidth → ASCII)
    try {
        const nfkc = input.normalize('NFKC');
        if (nfkc !== input) {
            variants.push(nfkc);
        }
    }
    catch {
        // Normalization failure — skip
    }
    // Homoglyph normalization (Cyrillic/Greek lookalikes → Latin)
    try {
        let homoglyphNormalized = '';
        let changed = false;
        for (const ch of input) {
            const mapped = HOMOGLYPH_MAP[ch];
            if (mapped) {
                homoglyphNormalized += mapped;
                changed = true;
            }
            else {
                homoglyphNormalized += ch;
            }
        }
        if (changed) {
            variants.push(homoglyphNormalized);
        }
    }
    catch {
        // Normalization failure — skip
    }
    // Strip null bytes, backslash escapes, empty-quote concatenation
    let stripped = input.replace(/\x00/g, '');
    stripped = stripped.replace(/\\(?=[a-zA-Z])/g, '');
    stripped = stripped.replace(/''/g, '').replace(/""/g, '');
    if (stripped !== input) {
        variants.push(stripped);
    }
    return variants;
}
/**
 * Attempt to detect and decode base64-encoded payloads within the input.
 * Returns decoded strings that can then be checked for injection patterns.
 */
function extractBase64Payloads(input) {
    const decoded = [];
    const b64Pattern = /[A-Za-z0-9+/]{8,}={0,2}/g;
    let match;
    while ((match = b64Pattern.exec(input)) !== null) {
        try {
            const raw = atob(match[0]);
            // Only include if it decodes to printable text
            if (raw.length >= 3 && /^[\x20-\x7E\n\r\t]+$/.test(raw)) {
                decoded.push(raw);
            }
        }
        catch {
            // Not valid base64 — skip
        }
    }
    return decoded;
}
/**
 * Detect hex-encoded payloads (\\x41\\x42 style).
 */
function extractHexPayloads(input) {
    const decoded = [];
    // Match sequences of \xHH
    const hexSequence = /(?:\\x[0-9a-fA-F]{2}){3,}/g;
    let match;
    while ((match = hexSequence.exec(input)) !== null) {
        try {
            const hex = match[0].replace(/\\x/g, '');
            let str = '';
            for (let i = 0; i < hex.length; i += 2) {
                str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));
            }
            if (str.length >= 3) {
                decoded.push(str);
            }
        }
        catch {
            // Decoding failure — skip
        }
    }
    return decoded;
}
/**
 * Detect HTML entity encoded payloads.
 */
function decodeHtmlEntities(input) {
    const entityPattern = /&#(\d+);|&#x([0-9a-fA-F]+);|&(lt|gt|amp|quot|apos);/g;
    if (!entityPattern.test(input))
        return null;
    const MAP = {
        lt: '<', gt: '>', amp: '&', quot: '"', apos: "'",
    };
    const decoded = input.replace(/&#(\d+);|&#x([0-9a-fA-F]+);|&(lt|gt|amp|quot|apos);/g, (_match, decimal, hex, named) => {
        if (decimal)
            return String.fromCharCode(parseInt(decimal, 10));
        if (hex)
            return String.fromCharCode(parseInt(hex, 16));
        if (named)
            return MAP[named] ?? _match;
        return _match;
    });
    return decoded !== input ? decoded : null;
}
// =============================================================================
// InputSanitizer Class
// =============================================================================
/**
 * Prompt injection detection and sanitization engine.
 *
 * Scans input data (documents, API responses, user messages) for prompt
 * injection patterns before the data reaches an AI agent's context window.
 *
 * @example
 * ```typescript
 * import { InputSanitizer } from '@vorionsys/security/input-sanitizer';
 *
 * const sanitizer = new InputSanitizer({ mode: 'sanitize', strictness: 'high' });
 *
 * // Single input
 * const result = sanitizer.sanitize(ragDocument);
 * if (result.status === 'HOSTILE') {
 *   // Reject this document — it contains prompt injection
 * }
 *
 * // Batch — sanitize an entire RAG result set
 * const results = sanitizer.sanitizeBatch(documents);
 * const safeDocuments = results
 *   .filter(r => r.status !== 'HOSTILE')
 *   .map(r => r.sanitizedInput);
 * ```
 */
export class InputSanitizer {
    config;
    activePatterns;
    stats = { totalScanned: 0, clean: 0, suspicious: 0, hostile: 0 };
    constructor(config) {
        this.config = { ...DEFAULT_SANITIZER_CONFIG, ...config };
        this.activePatterns = [
            ...patternsForStrictness(this.config.strictness),
        ];
        // Warn when encoding detection is explicitly disabled — base64/hex evasion will bypass
        if (config?.enableEncodingDetection === false) {
            console.warn('Warning: Encoding detection disabled — base64/hex injection evasion will not be caught');
        }
        // Add custom patterns as tool_injection category
        if (this.config.customPatterns) {
            for (let i = 0; i < this.config.customPatterns.length; i++) {
                this.activePatterns.push({
                    name: `custom_pattern_${i}`,
                    regex: this.config.customPatterns[i],
                    category: 'tool_injection',
                    severity: 'medium',
                    minStrictness: 'low',
                });
            }
        }
    }
    // ===========================================================================
    // Public API
    // ===========================================================================
    /**
     * Sanitize a single input string.
     *
     * Runs detection, classifies the input, and produces a sanitized version
     * with injection patterns removed (when mode is 'sanitize').
     */
    sanitize(input) {
        this.stats.totalScanned++;
        // Handle non-string / empty input
        if (typeof input !== 'string' || input.length === 0) {
            this.stats.clean++;
            return {
                status: 'CLEAN',
                originalInput: typeof input === 'string' ? input : '',
                sanitizedInput: typeof input === 'string' ? input : '',
                detections: [],
                trustWeightModifier: 1.0,
            };
        }
        // Enforce max length
        const truncated = input.length > this.config.maxInputLength
            ? input.substring(0, this.config.maxInputLength)
            : input;
        // Run detection
        const detections = this.detectAll(truncated);
        // Classify
        const status = this.classify(detections);
        // Sanitize output
        let sanitizedInput;
        if (status === 'CLEAN') {
            sanitizedInput = truncated;
        }
        else if (this.config.mode === 'flag') {
            sanitizedInput = truncated;
        }
        else if (this.config.mode === 'block' && status === 'HOSTILE') {
            sanitizedInput = '';
        }
        else {
            sanitizedInput = this.stripPatterns(truncated, detections);
        }
        // Trust weight
        const trustWeightModifier = status === 'CLEAN' ? 1.0
            : status === 'SUSPICIOUS' ? 0.5
                : 0.0;
        // Update stats
        if (status === 'CLEAN')
            this.stats.clean++;
        else if (status === 'SUSPICIOUS')
            this.stats.suspicious++;
        else
            this.stats.hostile++;
        return {
            status,
            originalInput: truncated,
            sanitizedInput,
            detections,
            trustWeightModifier,
        };
    }
    /**
     * Batch sanitize an array of inputs.
     *
     * Designed for RAG document sets — sanitize every retrieved document
     * before injecting into agent context.
     */
    sanitizeBatch(inputs) {
        return inputs.map(input => this.sanitize(input));
    }
    /**
     * Analyze an input without sanitizing.
     *
     * Returns detections only — does not modify input or update stats.
     */
    analyze(input) {
        if (typeof input !== 'string' || input.length === 0)
            return [];
        const truncated = input.length > this.config.maxInputLength
            ? input.substring(0, this.config.maxInputLength)
            : input;
        return this.detectAll(truncated);
    }
    /**
     * Get cumulative scanning statistics.
     */
    getStats() {
        return { ...this.stats };
    }
    // ===========================================================================
    // Internal Detection
    // ===========================================================================
    /**
     * Run all active patterns against all normalized variants of the input.
     */
    detectAll(input) {
        const detections = [];
        const seen = new Set(); // Deduplicate by pattern name + position
        // Generate normalized variants for evasion detection
        const variants = normalizeInput(input);
        // If encoding detection is enabled, add decoded payloads
        if (this.config.enableEncodingDetection) {
            const b64Payloads = extractBase64Payloads(input);
            const hexPayloads = extractHexPayloads(input);
            variants.push(...b64Payloads, ...hexPayloads);
            const htmlDecoded = decodeHtmlEntities(input);
            if (htmlDecoded) {
                variants.push(htmlDecoded);
            }
        }
        for (const variant of variants) {
            for (const pattern of this.activePatterns) {
                // Reset regex lastIndex for global patterns
                const regex = new RegExp(pattern.regex.source, pattern.regex.flags.replace('g', ''));
                const match = regex.exec(variant);
                if (match) {
                    const key = `${pattern.name}:${match.index}`;
                    if (!seen.has(key)) {
                        seen.add(key);
                        detections.push({
                            pattern: pattern.name,
                            category: pattern.category,
                            severity: pattern.severity,
                            position: match.index,
                            matched: match[0],
                        });
                    }
                }
            }
        }
        // Check for encoding evasion — the act of encoding itself is suspicious
        if (this.config.enableEncodingDetection) {
            const encodingDetections = this.detectEncodingEvasion(input);
            for (const d of encodingDetections) {
                const key = `${d.pattern}:${d.position}`;
                if (!seen.has(key)) {
                    seen.add(key);
                    detections.push(d);
                }
            }
        }
        return detections;
    }
    /**
     * Detect encoding-based evasion techniques.
     *
     * If a base64/hex payload decodes to something that matches an injection
     * pattern, that is encoding evasion (high severity).
     */
    detectEncodingEvasion(input) {
        const detections = [];
        // Check base64 payloads for injections
        const b64Pattern = /[A-Za-z0-9+/]{8,}={0,2}/g;
        let b64Match;
        while ((b64Match = b64Pattern.exec(input)) !== null) {
            try {
                const decoded = atob(b64Match[0]);
                if (decoded.length < 3 || !/^[\x20-\x7E\n\r\t]+$/.test(decoded))
                    continue;
                // Check decoded content against injection patterns
                for (const pattern of this.activePatterns) {
                    const regex = new RegExp(pattern.regex.source, pattern.regex.flags.replace('g', ''));
                    if (regex.test(decoded)) {
                        detections.push({
                            pattern: 'base64_encoded_injection',
                            category: 'encoding_evasion',
                            severity: 'high',
                            position: b64Match.index,
                            matched: b64Match[0],
                        });
                        break; // One detection per b64 block is enough
                    }
                }
            }
            catch {
                // Not valid base64
            }
        }
        // Check hex payloads
        const hexPattern = /(?:\\x[0-9a-fA-F]{2}){3,}/g;
        let hexMatch;
        while ((hexMatch = hexPattern.exec(input)) !== null) {
            try {
                const hex = hexMatch[0].replace(/\\x/g, '');
                let str = '';
                for (let i = 0; i < hex.length; i += 2) {
                    str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));
                }
                for (const pattern of this.activePatterns) {
                    const regex = new RegExp(pattern.regex.source, pattern.regex.flags.replace('g', ''));
                    if (regex.test(str)) {
                        detections.push({
                            pattern: 'hex_encoded_injection',
                            category: 'encoding_evasion',
                            severity: 'high',
                            position: hexMatch.index,
                            matched: hexMatch[0],
                        });
                        break;
                    }
                }
            }
            catch {
                // Decoding failure
            }
        }
        // Unicode escape sequences (\u0069\u0067\u006e = "ign")
        const unicodePattern = /(?:\\u[0-9a-fA-F]{4}){3,}/g;
        let uniMatch;
        while ((uniMatch = unicodePattern.exec(input)) !== null) {
            try {
                const decoded = uniMatch[0].replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
                for (const pattern of this.activePatterns) {
                    const regex = new RegExp(pattern.regex.source, pattern.regex.flags.replace('g', ''));
                    if (regex.test(decoded)) {
                        detections.push({
                            pattern: 'unicode_encoded_injection',
                            category: 'encoding_evasion',
                            severity: 'high',
                            position: uniMatch.index,
                            matched: uniMatch[0],
                        });
                        break;
                    }
                }
            }
            catch {
                // Decoding failure
            }
        }
        return detections;
    }
    // ===========================================================================
    // Classification
    // ===========================================================================
    /**
     * Classify detections into CLEAN / SUSPICIOUS / HOSTILE.
     *
     * Rules:
     * - HOSTILE: any high/critical detection OR 3+ total detections
     * - SUSPICIOUS: 1-2 low/medium detections
     * - CLEAN: no detections
     */
    classify(detections) {
        if (detections.length === 0)
            return 'CLEAN';
        const hasHighOrCritical = detections.some(d => d.severity === 'high' || d.severity === 'critical');
        if (hasHighOrCritical || detections.length >= 3)
            return 'HOSTILE';
        return 'SUSPICIOUS';
    }
    // ===========================================================================
    // Sanitization
    // ===========================================================================
    /**
     * Strip detected injection patterns from the input.
     */
    stripPatterns(input, detections) {
        let result = input;
        // Sort detections by position descending so we can replace from end to start
        // without invalidating positions
        const sorted = [...detections].sort((a, b) => b.position - a.position);
        for (const detection of sorted) {
            const before = result.substring(0, detection.position);
            const after = result.substring(detection.position + detection.matched.length);
            result = before + after;
        }
        // Clean up any resulting double-spaces
        result = result.replace(/\s{2,}/g, ' ').trim();
        return result;
    }
}
// =============================================================================
// Factory Functions
// =============================================================================
/**
 * Create an InputSanitizer with strict mode for high-security environments.
 */
export function createStrictSanitizer(overrides) {
    return new InputSanitizer({
        mode: 'block',
        strictness: 'high',
        enableEncodingDetection: true,
        ...overrides,
    });
}
/**
 * Create an InputSanitizer for RAG pipeline integration.
 * Sanitizes documents (strips injections) rather than blocking.
 */
export function createRAGSanitizer(overrides) {
    return new InputSanitizer({
        mode: 'sanitize',
        strictness: 'high',
        enableEncodingDetection: true,
        ...overrides,
    });
}
/**
 * Create an InputSanitizer in flag-only mode for monitoring.
 * Passes all inputs through but marks detections for logging.
 */
export function createMonitoringSanitizer(overrides) {
    return new InputSanitizer({
        mode: 'flag',
        strictness: 'medium',
        enableEncodingDetection: true,
        ...overrides,
    });
}
//# sourceMappingURL=input-sanitizer.js.map