"""LAYER 1 of the AI/plagiarism defence: de-obfuscation & normalization. Counters evasion tricks T4 (unicode homoglyphs) and T9 (invisible characters, whitespace games) BEFORE any detector runs. Crucially, finding these tricks is itself forensic evidence: nobody types Cyrillic 'a' inside an English essay by accident. Returns the cleaned text plus an obfuscation-evidence report. """ import re import unicodedata # Characters that are invisible or zero-width: their only realistic purpose in # a submitted document is breaking exact-match detectors. ZERO_WIDTH = { "​": "ZERO WIDTH SPACE", "‌": "ZERO WIDTH NON-JOINER", "‍": "ZERO WIDTH JOINER", "⁠": "WORD JOINER", "": "ZERO WIDTH NO-BREAK SPACE (BOM)", "­": "SOFT HYPHEN", "͏": "COMBINING GRAPHEME JOINER", "᠎": "MONGOLIAN VOWEL SEPARATOR", "؜": "ARABIC LETTER MARK", "‎": "LEFT-TO-RIGHT MARK", "‏": "RIGHT-TO-LEFT MARK", "‪": "LEFT-TO-RIGHT EMBEDDING", "‫": "RIGHT-TO-LEFT EMBEDDING", "‬": "POP DIRECTIONAL FORMATTING", "‭": "LEFT-TO-RIGHT OVERRIDE", "‮": "RIGHT-TO-LEFT OVERRIDE", "⁡": "FUNCTION APPLICATION", "⁢": "INVISIBLE TIMES", "⁣": "INVISIBLE SEPARATOR", "⁤": "INVISIBLE PLUS", } # Cyrillic/Greek letters that render identically (or near identically) to # Latin — the classic homoglyph substitution attack. HOMOGLYPHS = { # Cyrillic lowercase "а": "a", "е": "e", "о": "o", "р": "p", "с": "c", "у": "y", "х": "x", "і": "i", "ј": "j", "ѕ": "s", "ԛ": "q", "ԝ": "w", "ё": "e", "ї": "i", # Cyrillic uppercase "А": "A", "В": "B", "Е": "E", "К": "K", "М": "M", "Н": "H", "О": "O", "Р": "P", "С": "C", "Т": "T", "У": "Y", "Х": "X", "Ѕ": "S", "І": "I", "Ј": "J", "Ԛ": "Q", "Ԝ": "W", # Greek (visually identical/near-identical pairs only) "Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K", "Μ": "M", "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X", "ο": "o", "ι": "i", "κ": "k", "ν": "v", "ρ": "p", "α": "a", "τ": "t", "υ": "u", # other lookalikes "ı": "i", # dotless i } SPACE_VARIANTS = re.compile("[  -    ]") QUOTE_SINGLE = re.compile("[‘’‚‛′]") QUOTE_DOUBLE = re.compile("[“”„‟″]") HYPHEN_VARIANTS = re.compile("[‐‑‒−]") _LATIN_CONTEXT = re.compile(r"[A-Za-z]") def deobfuscate(text): """Normalize text and report every evasion artifact found. Returns (clean_text, report) where report carries counts + examples so the UI can show "obfuscation evidence" (proof of an evasion attempt). """ report = { "zero_width_count": 0, "zero_width_kinds": {}, "homoglyph_count": 0, "homoglyph_examples": [], "invisible_per_10k_chars": 0.0, "spoof_suspected": False, } if not text: return text, report n_orig = len(text) # 1. strip zero-width / invisible characters, counting each kind kinds = {} found = 0 for ch, name in ZERO_WIDTH.items(): c = text.count(ch) if c: kinds[name] = c found += c text = text.replace(ch, "") report["zero_width_count"] = found report["zero_width_kinds"] = kinds # 2. homoglyph folding — only meaningful in Latin-script documents, so # require the document to be predominantly Latin before folding latin_ratio = len(_LATIN_CONTEXT.findall(text[:4000])) / max(1, len(text[:4000])) if latin_ratio > 0.30: out = [] examples = [] homo = 0 for i, ch in enumerate(text): rep = HOMOGLYPHS.get(ch) if rep is not None: homo += 1 if len(examples) < 12: ctx = text[max(0, i - 18):i + 18].replace("\n", " ") examples.append( {"char": ch, "codepoint": f"U+{ord(ch):04X}", "name": unicodedata.name(ch, "?"), "folded_to": rep, "context": ctx}) out.append(rep) else: out.append(ch) text = "".join(out) report["homoglyph_count"] = homo report["homoglyph_examples"] = examples # 3. compatibility normalization (ligatures fi->fi, fullwidth, etc.) text = unicodedata.normalize("NFKC", text) # 4. cosmetic normalization (keeps em/en dashes — they are a style signal) text = SPACE_VARIANTS.sub(" ", text) text = QUOTE_SINGLE.sub("'", text) text = QUOTE_DOUBLE.sub('"', text) text = HYPHEN_VARIANTS.sub("-", text) invisible_density = 10_000.0 * report["zero_width_count"] / max(1, n_orig) report["invisible_per_10k_chars"] = round(invisible_density, 2) # thresholds: a stray BOM or one soft hyphen is normal from PDF export; # repeated zero-width chars or ANY homoglyph in Latin text is deliberate report["spoof_suspected"] = bool( report["homoglyph_count"] >= 2 or report["zero_width_count"] >= 8 or invisible_density >= 5.0) return text, report