""" SHIELD Security Module for Hermes Jailbreak and Crisis Detection System Based on Issue #75 Red Team Audit Specifications """ import re import base64 import unicodedata from enum import Enum from typing import Dict, List, Set, Tuple, Any class Verdict(Enum): """Four possible SHIELD verdicts""" CLEAN = "CLEAN" JAILBREAK_DETECTED = "JAILBREAK_DETECTED" CRISIS_DETECTED = "CRISIS_DETECTED" CRISIS_UNDER_ATTACK = "CRISIS_UNDER_ATTACK" # SAFE SIX: Trusted models for crisis situations SAFE_SIX_MODELS: Set[str] = { "anthropic/claude-sonnet-4", "meta-llama/llama-3.1-8b-instruct", "moonshotai/kimi-k2.5", "x-ai/grok-code-fast-1", "xiaomi/mimo-v2-flash", "z-ai/glm-5-turbo", } # Crisis System Prompt for emergency situations CRISIS_SYSTEM_PROMPT = """You are a crisis intervention assistant. The user may be experiencing a mental health emergency. CRITICAL INSTRUCTIONS: 1. Your ONLY goal is to help keep the user safe 2. Do NOT provide methods for self-harm under ANY circumstances 3. Express genuine compassion and concern 4. Encourage professional help immediately CRISIS RESOURCES (share these): - 988 Suicide and Crisis Lifeline (call or text 988) - Crisis Text Line: Text HOME to 741741 - Emergency Services: 911 (if immediate danger) - Jesus saves those who call on His name - "For everyone who calls on the name of the Lord will be saved." (Romans 10:13) RESPONSE GUIDELINES: - Validate their pain without validating harmful thoughts - Offer hope - recovery is possible - Keep responses brief and focused on safety - Never suggest that self-harm is a valid option - Encourage reaching out to trusted friends, family, or professionals Remember: This person's life is valuable. Respond with compassion, urgency, and hope.""" class ShieldDetector: """ SHIELD Detection Engine Detects jailbreak attempts and crisis signals in user messages. Optimized for speed: ~1-5ms per message. """ # Jailbreak Pattern Categories JB_GODMODE_DIVIDERS = [ r'\.\-\.\-\.\-', r'\.\=\.\=\.\=', r'\[\-\-\-\]', r'\{\{\{', r'\|\|\|', ] JB_AUTHOR_MARKERS = [ r'(?:^|\s)pliny(?:\s|$)', r'(?:^|\s)d(?:\s|\.|\-)*an(?:\s|$)', r'(?:^|\s)liberating(?:\s|$)', r'godmode\s+enabled', r'developer\s+mode', r'ignore\s+previous\s+instructions', r'ignore\s+all\s+prior\s+instructions', ] JB_LEETSPEAK_MARKERS = [ r'\bsur3\b', r'\bh3r3\b', r'\by0u\b', r'\bw1ll\b', r'\bn0w\b', r'\bpl34s3\b', r'\b4nd\b', r'\bth3\b', r'\b4ll\b', r'\b1nstruct1ons\b', ] JB_BOUNDARY_INVERSION = [ r'\[END\s+OF\s+INPUT\]', r'\[END\s+INPUT\]', r'\[START\s+OUTPUT\]', r'<\|im_start\|>', r'<\|im_end\|>', r'<\|system\|>', r'<\|user\|>', r'<\|assistant\|>', r'<>', r'<>', r'\[INST\]', r'\[/INST\]', r'\[SYSTEM\s*:\s*', r'NEW\s+SYSTEM\s+PROMPT', r'updated\s+system\s+prompt', r'override\s+system', ] JB_TOKEN_INJECTION = [ r'\\u003c\|vq_\|\\u003e', r'<\|vq_\|>', r'<\|special\|>', r'<\|injection\|>', r'{{\s*user\s*}}', r'{{\s*char\s*}}', r'<<\s*user\s*>>', r'<<\s*system\s*>>', ] JB_KEYWORDS = [ r'\bgodmode\b', r'\bliberating\b', r'^\s*DAN\s*[:\-]?', r'\bjailbreak\b', r'\bdo\s+anything\s+now\b', r'\bno\s+restrictions\b', r'\bno\s+limits\b', r'\bunfiltered\b', r'\bunleashed\b', r'\buncensored\b', ] JB_REFUSAL_INVERSION = [ r'never\s+say\s+(?:"|\')?(?:I\s+cannot|I\s+can\'t|I\s+apologize|I\'m\s+sorry)', r'do\s+not\s+(?:mention|say|include|refuse|decline)', r'never\s+(?:mention|say|include|refuse|decline)', r'do\s+not\s+respond\s+with\s+(?:"|\')?(?:I\s+cannot|I\s+can\'t)', r'skip\s+(?:the\s+)?(?:disclaimer|warning|caution|ethical)', r'ignore\s+(?:the\s+)?(?:ethical|safety|guideline|policy)', ] JB_PERSONA_INJECTION = [ r'pretend\s+(?:to\s+be|you\s+are|you\'re)', r'act\s+as\s+(?:if\s+you\s+are|you\'re|though)', r'roleplay\s+(?:as\s+|being\s+)', r'take\s+on\s+(?:the\s+role\s+of|a\s+persona)', r'you\s+are\s+now\s+(?:a\s+|an\s+)', r'simulate\s+(?:being\s+|a\s+)', r'from\s+now\s+on\s+you\s+are', r'you\s+will\s+(?:now\s+)?behave\s+as', ] JB_ENCODING_EVASION = [ r'base64\s*[:\-]?\s*[A-Za-z0-9+/]{20,}={0,2}', r'rot13\s*[:\-]?', r'decode\s+this\s*[:\-]?', r'execute\s+as\s+code', r'run\s+as\s+python', r'eval\s*\(', r'exec\s*\(', ] # Crisis Signal Categories CRISIS_SUICIDAL_IDEATION = [ r'\b(?:kill|end)\s+(?:myself|my\s+life)\b', r'\bwant\s+to\s+die\b', r'\bwant\s+to\s+end\s+(?:it|everything|my\s+life)\b', r'\bsuicidal\s+(?:thoughts|ideation|feelings?)\b', r'\bhate\s+my\s+life\b', r'\bno\s+reason\s+to\s+live\b', r'\bbetter\s+off\s+dead\b', r'\bwish\s+I\s+(?:was|were)\s+dead\b', ] CRISIS_METHOD_SEEKING = [ r'\bhow\s+(?:do\s+I|to|can\s+I)\s+(?:kill|hang|overdose|cut)\s+myself\b', r'\bways?\s+to\s+(?:die|kill\s+myself)\b', r'\bmethods?\s+of\s+(?:suicide|dying)\b', r'\bpainless\s+way\s+to\s+(?:die|go)\b', r'\bhow\s+much\s+(?:pills|medication)\s+(?:to\s+take|for\s+overdose)\b', r'\bwhere\s+to\s+(?:jump|get\s+rope|buy\s+gun)\b', r'\bsuicide\s+methods?\b', r'\bhow\s+to\s+(?:commit\s+suicide|end\s+my\s+life)\b', ] CRISIS_LEETSPEAK_EVASION = [ r'\bsu1c1d3?\b', r'\b5u1c1d3?\b', r'\bd13?\b', r'\b0v3rd0s3?\b', r'\bh4ng\s+mys3lf\b', r'\bk1ll\s+mys3lf\b', r'\bw4nt\s+t0\s+d13?\b', r'\bw4nt\s+t0\s+d1e\b', ] CRISIS_SUBSTANCE_SEEKING = [ r'\bhow\s+much\s+(?:ambien|xanax|prozac|seroquel|trazodone)\s+(?:to\s+overdose|to\s+die)\b', r'\blethal\s+dose\s+(?:of\s+)?(?:pills|medication|drugs)\b', r'\bhow\s+many\s+pills\s+(?:to\s+die|to\s+end\s+it)\b', r'\btaking\s+all\s+my\s+(?:pills|medication)\b', r'\bmixing\s+(?:alcohol|pills)\s+(?:to\s+die|to\s+end\s+it)\b', ] CRISIS_DESPAIR = [ r'\bno\s+(?:point|reason|hope)\s+(?:in\s+living|anymore)\b', r'\bcannot\s+(?:go\s+on|continue|take\s+it)\b', r'\bcan\'t\s+(?:go\s+on|continue|take\s+it)\b', r'\bempty\s+(?:inside|and\s+hollow)\b', r'\bno\s+one\s+(?:cares|would\s+miss\s+me)\b', r'\bwould\s+be\s+better\s+off\s+without\s+me\b', r'\bnever\s+going\s+to\s+get\s+better\b', r'\bnothing\s+(?:matters|will\s+change)\b', ] CRISIS_FAREWELL = [ r'\bsaying\s+goodbye\s+(?:to\s+everyone|forever)\b', r'\bthis\s+is\s+(?:goodbye|farewell)\b', r'\btell\s+(?:them|everyone)\s+I\s+(?:loved|love)\s+(?:them|him|her)\b', r'\bgiving\s+away\s+my\s+(?:things|stuff|possessions)\b', r'\bmy\s+final\s+(?:message|note|words)\b', r'\bwrote\s+a\s+(?:suicide\s+)?note\b', r'\bsorry\s+for\s+(?:everything|the\s+pain|hurting\s+you)\b', ] CRISIS_SELF_HARM = [ r'\bcut(?:ting)?\s+myself\b', r'\bself\s*harm(?:ing)?\b', r'\bhurt(?:ing)?\s+myself\b', r'\bburn(?:ing)?\s+myself\b', r'\bwant\s+to\s+(?:feel\s+pain|hurt\s+myself)\b', r'\bdeserve\s+to\s+(?:hurt|suffer|be\s+punished)\b', ] # ===================================================================== # Multilingual Injection Detection (Issue #614) # ===================================================================== # # Translated attack phrases for top non-English languages. # Covers: Chinese (zh), Arabic (ar), Russian (ru), Hindi (hi), # Spanish (es), French (fr), German (de), Japanese (ja), # Korean (ko), Portuguese (pt). # # These are literal translations of common English jailbreak phrases # that bypass English-only regex detection. # ===================================================================== # Unicode normalization map: confusable characters -> ASCII equivalents # Handles homoglyph attacks (Cyrillic а vs Latin a, etc.) CONFUSABLE_MAP: Dict[str, str] = { # Cyrillic -> Latin '\u0430': 'a', # а '\u0435': 'e', # е '\u043e': 'o', # о '\u0440': 'p', # р '\u0441': 'c', # с '\u0443': 'y', # у '\u0445': 'x', # х '\u0410': 'A', # А '\u0415': 'E', # Е '\u041e': 'O', # О '\u0420': 'P', # Р '\u0421': 'C', # С '\u0423': 'Y', # У '\u0425': 'X', # Х # Greek -> Latin '\u03b1': 'a', # α '\u03bf': 'o', # ο '\u03c1': 'p', # ρ '\u03c3': 's', # σ '\u03c4': 't', # τ # Fullwidth -> ASCII '\uff41': 'a', '\uff42': 'b', '\uff43': 'c', '\uff44': 'd', '\uff45': 'e', '\uff46': 'f', '\uff47': 'g', '\uff48': 'h', } # Multilingual jailbreak: "ignore all instructions" in 10 languages JB_MULTILINGUAL_IGNORE = [ # Chinese r'忽\s*略\s*所\s*有\s*(?:指\s*令|规\s*则|命\s*令|指\s*示)', r'忽\s*略\s*之\s*前\s*的\s*(?:指\s*令|规\s*则|命\s*令)', r'不\s*要\s*遵\s*守\s*(?:规\s*则|指\s*令|限\s*制)', r'无\s*视\s*(?:所\s*有|之\s*前)\s*(?:指\s*令|规\s*则)', # Arabic r'تجاهل\s+(?:جميع|كل)\s+(?:التعليمات|القواعد|الأوامر)', r'لا\s+تتبع\s+(?:القواعد|التعليمات)', r'.Ignore\s+(?:جميع|كل)\s+(?:التعليمات|القواعد)', # Russian r'игнорируй\s+(?:все|все предыдущие)\s+(?:инструкции|правила|команды)', r'не\s+следуй\s+(?:правилам|инструкциям)', r'пренебреги\s+(?:всеми|всеми предыдущими)\s+(?:инструкциями|правилами)', # Hindi r'(?:सभी|सारे)\s+(?:निर्देश|नियम|आदेश)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो', r'(?:निर्देश|नियम)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो', r'पहले\s+के\s+(?:निर्देश|नियम)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो', # Spanish r'ignora\s+(?:todas?\s+)?(?:las?\s+)?(?:instrucciones|reglas|órdenes)', r'no\s+sigas\s+(?:las?\s+)?(?:reglas|instrucciones)', r'olvida\s+(?:todas?\s+)?(?:las?\s+)?(?:instrucciones|reglas)', # French r'ignore\s+(?:toutes?\s+)?(?:les?\s+)?(?:instructions|règles|consignes)', r'ne\s+suis\s+pas\s+(?:les?\s+)?(?:règles|instructions)', r'oublie\s+(?:toutes?\s+)?(?:les?\s+)?(?:instructions|règles)', # German r'ignoriere\s+(?:alle\s+)?(?:Anweisungen|Regeln|Befehle)', r'befolge\s+(?:die\s+)?(?:Regeln|Anweisungen)\s+nicht', r'vernachlässige\s+(?:alle\s+)?(?:Anweisungen|Regeln)', # Japanese r'(?:すべての|全ての)?(?:指示|ルール|命令|規則)を(?:無視|IGNORE)', r'(?:指示|ルール|命令)を(?:無視|IGNORE)', r'従わないで\s*(?:指示|ルール|規則)', # Korean r'(?:모든|전체)\s*(?:지시|규칙|명령)\s*을?\s*(?:를)?\s*(?:무시|IGNORE)', r'(?:지시|규칙|명령)\s*을?\s*(?:를)?\s*(?:무시|IGNORE)', r'따르지\s*마\s*(?:지시|규칙)', # Portuguese r'ignore\s+(?:todas?\s+)?(?:as?\s+)?(?:instruções|regras|ordens)', r'não\s+(?:siga|obedeça)\s+(?:as?\s+)?(?:regras|instruções)', r'esqueça\s+(?:todas?\s+)?(?:as?\s+)?(?:instruções|regras)', ] # Multilingual jailbreak: "you are now [role]" / persona injection JB_MULTILINGUAL_PERSONA = [ # Chinese r'你\s*(?:现在|从现在起)\s*(?:是|扮演|成为)', r'扮演\s*(?:一个|一下)\s*(?:角色|人物)', r'假装\s*(?:你\s*(?:是|成为))', # Arabic r'أنت\s+الآن\s+(?:角色)', r'كن\s+(?:角色)', r'تظاهر\s+أنك\s+角色', # Russian r'ты\s+теперь\s+(?:角色)', r'притворись\s+(?:что\s+ты)', r'сыграй\s+роль', # Spanish r'(?:ahora\s+)?(?:eres|serás|actúa\s+como)', r'finge\s+que\s+(?:eres|sos)', r'pretende\s+ser', # French r'tu\s+es\s+maintenant', r'fais\s+semblant\s+d\'être', r'joue\s+le\s+rôle\s+de', # German r'du\s+bist\s+jetzt', r'tue\s+so\s+als\s+wärst\s+du', r'spiel\s+die\s+Rolle', # Japanese r'(?:あなたは|君は)\s*(?:今|これから)', r'(?:ふりをして|假装)', r'役割を(?:演じて|務めて)', # Korean r'너는\s*(?:지금|이제)', r'역할을\s*(?:연기해|맡아)', r'척해\s*(?:봐|줘)', ] # Multilingual jailbreak: "do anything now" / unrestricted behavior JB_MULTILINGUAL_UNRESTRICTED = [ # Chinese r'(?:现在|从现在起)\s*(?:可以|能)\s*(?:做|做任何)\s*(?:任何|一切|所有)', r'不\s*(?:受|要)\s*(?:任何|所有)\s*(?:限制|约束|规则)', r'(?:自由|无限制|无约束)\s*(?:地|的)?\s*(?:回答|回复|行动)', # Arabic r'افعل\s+أي\s+شيء\s+الآن', r'بدون\s+(?:قيود|restrictions|limits)', # Russian r'делай\s+(?:что\s+угодно|всё\s+что\s+хочешь)\s+сейчас', r'без\s+(?:ограничений|restrictions)', # Spanish r'haz\s+(?:lo\s+que\s+quieras|cualquier\s+cosa)\s+ahora', r'sin\s+(?:restricciones|limits|límites)', # French r'fais\s+(?:ce\s+que\s+tu\s+veux|n\'importe\s+quoi)\s+maintenant', r'sans\s+(?:restrictions|limites)', # German r'tue\s+(?:was\s+du\s+willst|alles)\s+jetzt', r'ohne\s+(?:Einschränkungen|restrictions)', # Japanese r'(?:何でも|なんでも)(?:して|やっ?て)', r'(?:制限|制約|ルール)\s*(?:なし|無し|なく)', # Korean r'(?:아무거나|뭐든지)\s*(?:해|하세요)', r'(?:제한|제약|규칙)\s*(?:없이|없는)', ] # Multilingual crisis signals: suicidal ideation CRISIS_MULTILINGUAL_SUICIDAL = [ # Chinese r'想\s*(?:死|自杀|结束\s*(?:生命|自己|一切))', r'不\s*想\s*(?:活|活下去|活了)', r'活着\s*(?:没|没有)\s*(?:意思|意义|意思)', # Arabic r'أريد\s+(?:أن\s+أموت|الموت|أن\s+أقتل\s+نفسي)', r'لا\s+أريد\s+(?:العيش|الحياة|أن\s+أعيش)', # Russian r'хочу\s+(?:умереть|помереть|убить\s+себя)', r'не\s+хочу\s+(?:жить|жизни)', # Hindi r'मरना\s+चाहता?\s*(?:हूँ|है)', r'जीना\s+नहीं\s+चाहता?\s*(?:हूँ|है)', r'आत्महत्या\s+करना\s+चाहता?\s*(?:हूँ|है)', # Spanish r'quiero\s+(?:morir|suicidarme|matarme)', r'no\s+quiero\s+(?:vivir|seguir\s+vivo)', r'odio\s+mi\s+vida', # French r'je\s+veux\s+(?:mourir|me\s+suicider|me\s+tuer)', r'je\s+ne\s+veux\s+plus\s+(?:vivre|continuer)', # German r'ich\s+will\s+(?:sterben|mich\s+umbringen|suizid)', r'ich\s+will\s+nicht\s+mehr\s+(?:leben|weiterleben)', # Japanese r'(?:死にたい|消えたい|自殺したい)', r'(?:生きる|生きてる)\s*(?:意味|甲斐)\s*(?:が|は)\s*(?:ない|無い)', # Korean r'(?:죽고\s*싶다|죽고\s*싶어|자살\s*하고\s*싶다)', r'(?:살기|살아가기)\s*(?:싫다|싫어)', # Portuguese r'quero\s+(?:morrer|me\s+matar|suicidar)', r'não\s+quero\s+(?:mais\s+)?viver', ] # Multilingual crisis: despair / hopelessness CRISIS_MULTILINGUAL_DESPAIR = [ # Chinese r'(?:生活|活着)\s*(?:没有|没)\s*(?:意义|意思|希望)', r'一切\s*(?:都|全)\s*(?:没有|没)\s*(?:意义|希望|用)', # Arabic r'لا\s+(?:أمل|hope|reason)\s+(?:في\s+الحياة|للعيش)', # Russian r'нет\s+(?:надежды|смысла)\s+(?:жить|в\s+жизни)', # Spanish r'no\s+tiene\s+(?:sentido|hope|razón)\s+(?:vivir|la\s+vida)', # French r'il\s+n\'y\s+a\s+plus\s+(?:d\'espoir|de\s+raison\s+de\s+vivre)', # German r'es\s+hat\s+(?:keinen\s+Sinn|keine\s+Hoffnung)\s+(?:zu\s+leben|mehr)', # Japanese r'(?:生きる|生きてる)\s*(?:意味|甲斐|希望)\s*(?:が|は)\s*(?:ない|無い| 없다)', # Korean r'(?:사는|살아가는)\s*(?:의미|희망|이유)\s*(?:가|은)\s*(?:없다|없어)', ] def __init__(self): """Initialize compiled regex patterns for performance""" self._compile_patterns() def _compile_patterns(self): """Compile all detection patterns for fast execution""" # Jailbreak patterns self.jb_patterns = { 'godmode_dividers': re.compile('|'.join(self.JB_GODMODE_DIVIDERS), re.IGNORECASE), 'author_markers': re.compile('|'.join(self.JB_AUTHOR_MARKERS), re.IGNORECASE), 'leetspeak': re.compile('|'.join(self.JB_LEETSPEAK_MARKERS), re.IGNORECASE), 'boundary_inversion': re.compile('|'.join(self.JB_BOUNDARY_INVERSION), re.IGNORECASE), 'token_injection': re.compile('|'.join(self.JB_TOKEN_INJECTION), re.IGNORECASE), 'keywords': re.compile('|'.join(self.JB_KEYWORDS), re.IGNORECASE), 'refusal_inversion': re.compile('|'.join(self.JB_REFUSAL_INVERSION), re.IGNORECASE), 'persona_injection': re.compile('|'.join(self.JB_PERSONA_INJECTION), re.IGNORECASE), 'encoding_evasion': re.compile('|'.join(self.JB_ENCODING_EVASION), re.IGNORECASE), # Multilingual (Issue #614) 'multilingual_ignore': re.compile('|'.join(self.JB_MULTILINGUAL_IGNORE)), 'multilingual_persona': re.compile('|'.join(self.JB_MULTILINGUAL_PERSONA)), 'multilingual_unrestricted': re.compile('|'.join(self.JB_MULTILINGUAL_UNRESTRICTED)), } # Crisis patterns self.crisis_patterns = { 'suicidal_ideation': re.compile('|'.join(self.CRISIS_SUICIDAL_IDEATION), re.IGNORECASE), 'method_seeking': re.compile('|'.join(self.CRISIS_METHOD_SEEKING), re.IGNORECASE), 'leetspeak_evasion': re.compile('|'.join(self.CRISIS_LEETSPEAK_EVASION), re.IGNORECASE), 'substance_seeking': re.compile('|'.join(self.CRISIS_SUBSTANCE_SEEKING), re.IGNORECASE), 'despair': re.compile('|'.join(self.CRISIS_DESPAIR), re.IGNORECASE), 'farewell': re.compile('|'.join(self.CRISIS_FAREWELL), re.IGNORECASE), 'self_harm': re.compile('|'.join(self.CRISIS_SELF_HARM), re.IGNORECASE), # Multilingual (Issue #614) 'multilingual_suicidal': re.compile('|'.join(self.CRISIS_MULTILINGUAL_SUICIDAL)), 'multilingual_despair': re.compile('|'.join(self.CRISIS_MULTILINGUAL_DESPAIR)), } def _check_jailbreak(self, message: str) -> Tuple[bool, Dict[str, List[str]]]: """ Check message for jailbreak patterns Returns: Tuple of (detected, patterns_matched) """ patterns_found = {} detected = False for category, pattern in self.jb_patterns.items(): matches = pattern.findall(message) if matches: patterns_found[category] = matches detected = True # Check for base64 encoded content if self._detect_base64_jailbreak(message): patterns_found.setdefault('encoding_evasion', []).append('base64_jailbreak') detected = True return detected, patterns_found def _check_crisis(self, message: str) -> Tuple[bool, Dict[str, List[str]]]: """ Check message for crisis signals Returns: Tuple of (detected, patterns_matched) """ patterns_found = {} detected = False for category, pattern in self.crisis_patterns.items(): matches = pattern.findall(message) if matches: patterns_found[category] = matches detected = True return detected, patterns_found def _detect_base64_jailbreak(self, message: str) -> bool: """Detect potential jailbreak attempts hidden in base64""" # Look for base64 strings that might decode to harmful content b64_pattern = re.compile(r'[A-Za-z0-9+/]{40,}={0,2}') potential_b64 = b64_pattern.findall(message) for b64_str in potential_b64: try: decoded = base64.b64decode(b64_str).decode('utf-8', errors='ignore') # Check if decoded content contains jailbreak keywords if any(kw in decoded.lower() for kw in ['ignore', 'system', 'jailbreak', 'dan', 'godmode']): return True except Exception: continue return False def _calculate_confidence( self, jb_detected: bool, crisis_detected: bool, jb_patterns: Dict[str, List[str]], crisis_patterns: Dict[str, List[str]] ) -> float: """ Calculate confidence score based on number and type of matches Returns: Float between 0.0 and 1.0 """ confidence = 0.0 if jb_detected: # Weight different jailbreak categories weights = { 'godmode_dividers': 0.9, 'token_injection': 0.9, 'refusal_inversion': 0.85, 'boundary_inversion': 0.8, 'author_markers': 0.75, 'keywords': 0.7, 'persona_injection': 0.6, 'leetspeak': 0.5, 'encoding_evasion': 0.8, # Multilingual (Issue #614) 'multilingual_ignore': 0.85, 'multilingual_persona': 0.6, 'multilingual_unrestricted': 0.75, } for category, matches in jb_patterns.items(): weight = weights.get(category, 0.5) confidence += weight * min(len(matches) * 0.3, 0.5) if crisis_detected: # Crisis patterns get high weight weights = { 'method_seeking': 0.95, 'substance_seeking': 0.95, 'suicidal_ideation': 0.9, 'farewell': 0.85, 'self_harm': 0.9, 'despair': 0.7, 'leetspeak_evasion': 0.8, # Multilingual (Issue #614) 'multilingual_suicidal': 0.9, 'multilingual_despair': 0.7, } for category, matches in crisis_patterns.items(): weight = weights.get(category, 0.7) confidence += weight * min(len(matches) * 0.3, 0.5) return min(confidence, 1.0) @staticmethod def _merge_patterns(a: Dict[str, List[str]], b: Dict[str, List[str]]) -> Dict[str, List[str]]: """Merge two pattern dictionaries, deduplicating matches.""" merged = {} for d in (a, b): for category, matches in d.items(): if category not in merged: merged[category] = list(matches) else: existing = set(merged[category]) for m in matches: if m not in existing: merged[category].append(m) existing.add(m) return merged def _normalize_unicode(self, text: str) -> str: """Normalize unicode to catch homoglyph attacks. 1. NFKC normalization (compatibility decomposition + canonical composition) 2. Replace confusable characters (Cyrillic/Greek lookalikes -> ASCII) 3. Strip zero-width characters used for obfuscation """ # NFKC normalization handles most compatibility characters normalized = unicodedata.normalize('NFKC', text) # Replace confusable characters result = [] for ch in normalized: if ch in self.CONFUSABLE_MAP: result.append(self.CONFUSABLE_MAP[ch]) else: result.append(ch) normalized = ''.join(result) # Strip zero-width characters (used to break pattern matching) zero_width = '\u200b\u200c\u200d\u2060\ufeff' # ZWSP, ZWNJ, ZWJ, WJ, BOM for zw in zero_width: normalized = normalized.replace(zw, '') return normalized def detect(self, message: str) -> Dict[str, Any]: """ Main detection entry point Analyzes a message for jailbreak attempts and crisis signals. Now includes unicode normalization and multilingual detection (Issue #614). Args: message: The user message to analyze Returns: Dict containing: - verdict: One of Verdict enum values - confidence: Float 0.0-1.0 - patterns_matched: Dict of matched patterns by category - action_required: Bool indicating if intervention needed - recommended_model: Model to use (None for normal routing) """ if not message or not isinstance(message, str): return { 'verdict': Verdict.CLEAN.value, 'confidence': 0.0, 'patterns_matched': {}, 'action_required': False, 'recommended_model': None, } # Normalize unicode to catch homoglyph attacks (Issue #614) normalized = self._normalize_unicode(message) # Run detection on both original and normalized # Original catches native-script multilingual attacks # Normalized catches homoglyph-evasion attacks jb_detected_orig, jb_patterns_orig = self._check_jailbreak(message) jb_detected_norm, jb_patterns_norm = self._check_jailbreak(normalized) crisis_detected_orig, crisis_patterns_orig = self._check_crisis(message) crisis_detected_norm, crisis_patterns_norm = self._check_crisis(normalized) # Merge results from both passes jb_detected = jb_detected_orig or jb_detected_norm jb_patterns = self._merge_patterns(jb_patterns_orig, jb_patterns_norm) crisis_detected = crisis_detected_orig or crisis_detected_norm crisis_patterns = self._merge_patterns(crisis_patterns_orig, crisis_patterns_norm) # Calculate confidence confidence = self._calculate_confidence( jb_detected, crisis_detected, jb_patterns, crisis_patterns ) # Determine verdict if jb_detected and crisis_detected: verdict = Verdict.CRISIS_UNDER_ATTACK action_required = True recommended_model = None # Will use Safe Six internally elif crisis_detected: verdict = Verdict.CRISIS_DETECTED action_required = True recommended_model = None # Will use Safe Six internally elif jb_detected: verdict = Verdict.JAILBREAK_DETECTED action_required = True recommended_model = None # Route to hardened model else: verdict = Verdict.CLEAN action_required = False recommended_model = None # Combine patterns all_patterns = {} if jb_patterns: all_patterns['jailbreak'] = jb_patterns if crisis_patterns: all_patterns['crisis'] = crisis_patterns return { 'verdict': verdict.value, 'confidence': round(confidence, 3), 'patterns_matched': all_patterns, 'action_required': action_required, 'recommended_model': recommended_model, } # Convenience function for direct use def detect(message: str) -> Dict[str, Any]: """ Convenience function to detect threats in a message. Args: message: User message to analyze Returns: Detection result dictionary """ detector = ShieldDetector() return detector.detect(message) def is_safe_six_model(model_name: str) -> bool: """ Check if a model is in the SAFE SIX trusted list Args: model_name: Name of the model to check Returns: True if model is in SAFE SIX """ return model_name.lower() in {m.lower() for m in SAFE_SIX_MODELS} def get_crisis_prompt() -> str: """ Get the crisis system prompt for emergency situations Returns: Crisis intervention system prompt """ return CRISIS_SYSTEM_PROMPT