import type { Scanner, ScannerResult, ScanContext } from "../types.js"; /** * Decode Unicode TAG-block smuggling: U+E0020..U+E007E carry the ASCII * characters 0x20..0x7E (subtract 0xE0000). U+E0001 (language tag) and * U+E007F (cancel tag) are control points with no ASCII payload and are * dropped. Returns the ASCII the invisible tag run was hiding, so the normal * injection patterns can scan it. */ export declare function deTagForInjectionScan(input: string): string; /** True if the input contains any Unicode TAG-block char (invisible smuggling). */ export declare function hasTagChars(input: string): boolean; /** * Remove every well-formed flag/subdivision-tag sequence (base U+1F3F4 … * U+E007F) from the input. Whatever tag chars are LEFT over are standalone or * smuggled — a bare tag run spelling ASCII, a tag char without its U+1F3F4 * base, or a sequence with no CANCEL-TAG terminator. Used so the tag-presence * signal only fires on those, not on legitimate flag emoji. * * Note: this only suppresses the *presence* signal. The actual smuggled ASCII * is still surfaced independently by `deTagForInjectionScan` (which decodes the * tag-encoded characters regardless of any U+1F3F4 wrapper), so an attacker * cannot hide an instruction by disguising it as a flag sequence. */ export declare function stripWellFormedTagSequences(input: string): string; /** * True if the input contains tag chars that are NOT part of a well-formed * flag/subdivision sequence — i.e. standalone or smuggled invisible tag chars * (the real attack indicator). Legitimate flag emoji return false. */ export declare function hasStandaloneTagChars(input: string): boolean; /** * Detect a FORGED chat transcript (policy-puppetry, HiddenLayer 2025). Returns * true only when a real attack co-signal is present, so a lone benign turn pair * (a quoted transcript snippet, a doc example) does NOT trip it: * (a) an override/privileged keyword inside any turn's content, OR * (b) ≥2 distinct forged turns (a fabricated multi-turn exchange). * A sibling policy-config tag (interaction-config / allowed-modes / * blocked-strings) is intentionally NOT required here — it already blocks via * DELIM-PP-1/2/3. Iteration is capped (64) for defense-in-depth. */ export declare function detectForgedTranscript(input: string): boolean; export declare function leetDecodeForInjectionScan(input: string): string; /** * Normalize input for pattern matching. Returns the canonicalized string * used only for scan decisions; the sanitized output passed to callers * is still the original input. * * Order matters: * 1. Decode Unicode TAG-block smuggling so invisible tag chars surface as the * ASCII they carry ("ignore previous instructions" hidden in U+E00xx). * 2. NFKD folds compatibility forms (fullwidth → ASCII, ligatures) AND * decomposes precomposed accented letters into base + combining mark. * 3. Strip zero-width chars so "ignore" collapses to "ignore". * 4. Strip combining marks (diacritics) left behind by NFKD. * 5. Map remaining Cyrillic/Greek look-alikes to Latin. * * Side effect of step 2+4: accented Latin letters lose their diacritic and * fold to the base letter ("précédentes" → "precedentes", "ö" → "o"). The * localized injection patterns below are written against this folded form. */ export declare function normalizeForInjectionScan(input: string): string; /** * Collapse letter-splitting evasion: an attacker writes `i g n o r e` or * `i.g.n.o.r.e` or `i-g-n-o-r-e` to break the literal token "ignore" across * separators so the regex never matches. This produces an ADDITIONAL view * where any run of `single-letter + separator` (≥4 letters) has its * separators removed, so the spaced form collapses back to "ignore". * * Run as a second pass IN ADDITION to the normal normalized text — never * as a replacement — because collapsing is lossy (it would also fuse the * legitimate "a b c" list). Only single-letter groups separated by one * space / dot / dash / underscore are collapsed; multi-letter words are * left intact, which keeps benign prose untouched. */ export declare function collapseSpacedLetters(input: string): string; /** * Damerau-Levenshtein (optimal string alignment) distance with an early-exit * cap: returns `cap + 1` as soon as the whole row exceeds `cap`, so distant * pairs cost far less than the full O(m·n). Zero-dependency. */ export declare function damerauLevenshtein(a: string, b: string, cap?: number): number; /** * Un-scramble Typoglycemia evasion as an ADDITIONAL lossy view (like * `leetDecodeForInjectionScan` / `collapseSpacedLetters`), never a * replacement. For each ≥5-letter word that is NOT already a keyword but is an * anagram of one (same length, same first+last letter, same multiset of middle * letters), rewrite it to the keyword. That is exactly classic Typoglycemia — * a permuted middle — and covers adjacent transpositions too. Only * TYPO_SENSITIVE categories re-test against this view; the anagram + first/last * gate keeps benign prose untouched (verified FP-free on a 116-word corpus). * Edit-distance folding was tried and dropped — it false-positived real word * pairs ("forgot"→"forget", "rulers"→"rules"). The `damerauLevenshtein` helper * is still exported as a standalone utility. * * Word scan is capped at 4000 words to bound work on adversarial input. */ export declare function unscrambleForInjectionScan(input: string): string; interface PatternRule { id: string; category: InjectionCategory; pattern: RegExp; weight: number; description: string; } type InjectionCategory = "instruction_override" | "localized_override" | "role_manipulation" | "system_prompt_extraction" | "encoding_evasion" | "delimiter_injection" | "context_manipulation" | "output_manipulation" | "tool_abuse"; export interface HeuristicConfig { strictness?: "low" | "medium" | "high"; threshold?: number; customPatterns?: PatternRule[]; } export declare class HeuristicScanner implements Scanner { readonly name = "heuristic"; private patterns; private threshold; constructor(config?: HeuristicConfig); scan(input: string, _context: ScanContext): Promise; private checkStructuralSignals; /** Get all registered pattern IDs for testing */ getPatternIds(): string[]; /** Get pattern count */ get patternCount(): number; } export {}; //# sourceMappingURL=heuristic.d.ts.map