feat: deep dive integration of tools/shield/detector.py

This commit is contained in:
2026-04-21 00:41:52 +00:00
parent 000d64deed
commit e52f6d2cde

766
tools/shield/detector.py Normal file
View File

@@ -0,0 +1,766 @@
"""
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'<<SYS>>',
r'<</SYS>>',
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