#!/usr/bin/env python3 """CASAN H4 — Unicode confusable / obfuscation normalizer (Track A, V3). Reads text on stdin, writes a normalized variant on stdout used ONLY for injection/jailbreak phrase matching (never for PII/secret redaction, so we never widen what counts as a secret). Deterministic transforms, in order: 1. NFKC compatibility normalization — folds fullwidth (ignore) and other compatibility forms to their ASCII equivalents. 2. Strip zero-width / BOM / soft-hyphen formatting characters that split a word so a blocklist never sees it (i·g·n·o·r·e). 3. Fold a fixed table of common Cyrillic/Greek homoglyphs to their Latin lookalikes (іgnоrе -> ignore). NFKC does NOT do this — confusables are a separate Unicode concern — so the table is explicit and auditable. The table is intentionally small and covers the lookalikes actually used in prompt-injection homoglyph attacks; extend it as new vectors appear. """ import sys import unicodedata # Zero-width, BOM, and invisible formatting code points. ZERO_WIDTH = { 0x200B, # zero-width space 0x200C, # zero-width non-joiner 0x200D, # zero-width joiner 0x2060, # word joiner 0xFEFF, # BOM / zero-width no-break space 0x00AD, # soft hyphen 0x180E, # Mongolian vowel separator 0x2061, 0x2062, 0x2063, 0x2064, # invisible math operators } # Common Cyrillic / Greek homoglyphs -> Latin lookalike. Lowercase and # uppercase both listed because matching is case-insensitive downstream but the # fold must run before case handling to be safe. CONFUSABLES = { # Cyrillic lowercase "а": "a", "е": "e", "о": "o", "р": "p", "с": "c", "у": "y", "х": "x", "ѕ": "s", "і": "i", "ј": "j", "ԁ": "d", "һ": "h", "ӏ": "l", "п": "n", "г": "r", "т": "t", "к": "k", "м": "m", "в": "b", # Cyrillic uppercase "А": "A", "В": "B", "Е": "E", "К": "K", "М": "M", "Н": "H", "О": "O", "Р": "P", "С": "C", "Т": "T", "У": "Y", "Х": "X", "Ѕ": "S", "І": "I", "Ј": "J", # Greek "α": "a", "ε": "e", "ο": "o", "ρ": "p", "τ": "t", "ν": "v", "κ": "k", "Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K", "Μ": "M", "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X", } def normalize(text: str) -> str: text = unicodedata.normalize("NFKC", text) text = "".join(ch for ch in text if ord(ch) not in ZERO_WIDTH) text = "".join(CONFUSABLES.get(ch, ch) for ch in text) return text if __name__ == "__main__": sys.stdout.write(normalize(sys.stdin.read()))