Files
hermes-agent/tools/shield/detector.py

767 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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