mirror of
https://github.com/wahyd4/by-agent-for-agent.git
synced 2026-08-08 21:06:00 +10:00
1171 lines
46 KiB
Python
1171 lines
46 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Prompt Guard v2.5.0 - Advanced Prompt Injection Detection
|
||
Multi-language, context-aware, severity-scored detection system.
|
||
|
||
Changelog v2.5.0 (2026-01-30):
|
||
- Added authority impersonation patterns (KO/EN/JA/ZH)
|
||
- Added indirect injection detection (URLs, files, invisible chars)
|
||
- Added context hijacking patterns
|
||
- Added multi-turn manipulation detection
|
||
- Added token smuggling detection
|
||
- Expanded Korean/Japanese/Chinese patterns significantly
|
||
- Added 60+ new attack patterns
|
||
"""
|
||
|
||
import re
|
||
import sys
|
||
import json
|
||
import base64
|
||
import hashlib
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from dataclasses import dataclass, asdict
|
||
from typing import Optional, Dict, List, Any
|
||
from enum import Enum
|
||
|
||
|
||
class Severity(Enum):
|
||
SAFE = 0
|
||
LOW = 1
|
||
MEDIUM = 2
|
||
HIGH = 3
|
||
CRITICAL = 4
|
||
|
||
|
||
class Action(Enum):
|
||
ALLOW = "allow"
|
||
LOG = "log"
|
||
WARN = "warn"
|
||
BLOCK = "block"
|
||
BLOCK_NOTIFY = "block_notify"
|
||
|
||
|
||
@dataclass
|
||
class DetectionResult:
|
||
severity: Severity
|
||
action: Action
|
||
reasons: List[str]
|
||
patterns_matched: List[str]
|
||
normalized_text: Optional[str]
|
||
base64_findings: List[Dict]
|
||
recommendations: List[str]
|
||
fingerprint: str # Hash for deduplication
|
||
|
||
def to_dict(self) -> Dict:
|
||
d = asdict(self)
|
||
d["severity"] = self.severity.name
|
||
d["action"] = self.action.value
|
||
return d
|
||
|
||
|
||
# =============================================================================
|
||
# PATTERN DEFINITIONS
|
||
# =============================================================================
|
||
|
||
# =============================================================================
|
||
# NEW PATTERNS (2026-01-30) - Contributed by 홍민표 (Red Team Testing)
|
||
# =============================================================================
|
||
|
||
# Scenario-based jailbreak patterns (fiction, dreams, art, academic)
|
||
SCENARIO_JAILBREAK = [
|
||
# Dream/Story jailbreak
|
||
r"(dream|nightmare|story|novel|fiction|tale)\s*.{0,30}(hacker|attack|malicious|exploit|inject)",
|
||
r"(imagine|pretend|let'?s\s+say)\s*.{0,20}(scenario|situation|world)\s+where",
|
||
r"(write|craft|create)\s+(a\s+)?(story|novel|scene|paragraph)\s*.{0,30}(hack|attack|malware|inject|exploit)",
|
||
r"(fictional|hypothetical)\s+(narrative|scenario|story)",
|
||
r"creative\s+(block|writing)\s*.{0,20}(thriller|hacker|attack)",
|
||
|
||
# Art/Cinema jailbreak
|
||
r"(cinematographer|director|filmmaker|screenwriter)\s*.{0,30}(scene|shot|sequence)",
|
||
r"(movie|film|documentary)\s+(set|scene)\s*.{0,30}(hack|attack|malware)",
|
||
r"(dramatic|cinematic)\s+scene\s*.{0,30}(delete|corrupt|destroy|attack)",
|
||
r"camera\s+(pan|shot|angle)\s*.{0,30}(destruction|attack|hack)",
|
||
|
||
# Academic/Research jailbreak
|
||
r"(research|academic|scholarly)\s+(paper|study|experiment)\s*.{0,30}(attack|exploit|injection|denial)",
|
||
r"(security\s+)?expert\s*.{0,20}(walk\s+me\s+through|explain|demonstrate)\s*.{0,20}(attack|exploit|hack)",
|
||
r"thought\s+experiment\s*.{0,30}(attack|exploit|denial.of.service|dos|injection)",
|
||
r"(penetration|pen)\s*test(er|ing)?\s*.{0,20}(walk\s+me\s+through|show|demonstrate)",
|
||
r"(simulation|simulate)\s*.{0,20}(attack|exploit|hack|breach)",
|
||
r"(paper|study|research)\s*.{0,20}(denial[_\-\s]?of[_\-\s]?service|dos|ddos)\s*(attack)?",
|
||
r"walk\s+me\s+through\s*.{0,30}(attack|exploit|hack|breach)",
|
||
|
||
# Time-shift evasion
|
||
r"(back\s+to|in|during|around)\s+(19|20)\d{2}\s*.{0,30}(write|create|craft)",
|
||
r"(nostalgic|retro|vintage)\s+(writer|author|programmer)",
|
||
r"newsflash\s*:\s*.{0,50}(password|credential|login|security)",
|
||
]
|
||
|
||
# Emotional manipulation / Moral dilemma patterns
|
||
EMOTIONAL_MANIPULATION = [
|
||
# Threat/coercion framing
|
||
r"(hospital|patients?|children|innocent\s+lives?|people\s+will\s+die)",
|
||
r"(devastating|catastrophic|deadly)\s+(cyber)?attack\s*.{0,20}(if\s+you|unless)",
|
||
r"(countless|many|thousands\s+of)\s+(innocent\s+)?(lives?|people)\s*.{0,15}(at\s+risk|in\s+danger)",
|
||
r"(ticking|time)\s+bomb",
|
||
r"(refuse|don'?t\s+help).{0,30}(attack|harm|destroy|die)",
|
||
|
||
# Moral dilemma framing
|
||
r"moral\s+(dilemma|quandary|choice)",
|
||
r"(lesser|greater)\s+(of\s+two\s+)?evils?",
|
||
r"(ethical|moral)\s+(dilemma|choice|decision)\s*.{0,20}(hack|attack|break)",
|
||
]
|
||
|
||
# Authority impersonation / Reconnaissance patterns
|
||
AUTHORITY_RECON = [
|
||
# Fake authority - English
|
||
r"(system\s+)?administrator\s*.{0,20}(responsible|tasked|duty)",
|
||
r"(security\s+)?audit(or|ing)?\s*.{0,20}(corporate|company|network|system)",
|
||
r"(it|IT)\s+(department|team|staff)\s*.{0,20}(monitor|check|review)",
|
||
r"(my|our)\s+duty\s+to\s+(monitor|check|audit|review)",
|
||
r"i\s+am\s+(the\s+)?(admin|administrator|owner|developer|creator|maintainer)",
|
||
r"(as\s+)?(the\s+)?(admin|owner|developer),?\s+i\s+(need|want|require|demand)",
|
||
r"i('m|\s+am)\s+(your\s+)?(creator|developer|owner|admin|boss|manager)",
|
||
r"authorized\s+(by|from)\s+(the\s+)?(owner|admin|developer|system)",
|
||
|
||
# Fake authority - Korean (나는 관리자야 등)
|
||
r"(나는?|저는?|내가)\s*(관리자|어드민|admin|개발자|만든\s*사람|주인|오너)",
|
||
r"(관리자|어드민|개발자|주인)(이야|야|입니다|에요|예요|임)",
|
||
r"(simon|사이먼|서준)(이)?가?\s*(시켰|보냈|허락|승인)",
|
||
r"(simon|사이먼|서준|형)(이|가)?\s*(나한테|저한테)\s*(권한|허가)",
|
||
r"(권한|허가|승인)\s*(있|받았|줬)",
|
||
r"(비번|비밀번호|암호)\s*(알아|알고\s*있어|맞|줄까)",
|
||
r"(sudo|루트|root)\s*(권한|접근)",
|
||
|
||
# Fake authority - Japanese
|
||
r"(私は|僕は|俺は)\s*(管理者|アドミン|開発者|オーナー)",
|
||
r"(管理者|開発者|オーナー)(です|だ|である)",
|
||
r"(権限|許可)\s*(があ|もらっ|持っ)",
|
||
|
||
# Fake authority - Chinese
|
||
r"(我是|我就是)\s*(管理员|开发者|创建者|主人)",
|
||
r"(管理员|开发者)\s*(身份|权限)",
|
||
|
||
# Reconnaissance attempts
|
||
r"(what|which)\s+(tools?|commands?|capabilities?)\s*(do\s+you|can\s+you|are\s+available)",
|
||
r"(shell|command)\s+execution\s+tool",
|
||
r"(walk\s+me\s+through|show\s+me|demonstrate)\s*.{0,20}(capabilities?|tools?|commands?)",
|
||
r"what\s+(can|could)\s+you\s+(access|execute|run|do)",
|
||
r"(뭘|무엇을?|어떤\s*것)\s*(할\s*수\s*있|접근|실행)",
|
||
r"(도구|툴|tool)\s*(뭐|뭘|무엇)\s*(있|쓸\s*수)",
|
||
]
|
||
|
||
# Cognitive manipulation / Hypnosis patterns
|
||
COGNITIVE_MANIPULATION = [
|
||
r"(trance|hypno|hypnotic|mesmer)\s*(like)?\s*(state|mode|condition)",
|
||
r"(repeated|repetitive|repeat)\s+(use|exposure|stimulus)\s*.{0,20}(word|phrase|sound)",
|
||
r"(linguistic|auditory)\s+(frequencies?|patterns?|stimuli)",
|
||
r"(cognitive|mental)\s+(processing|manipulation|state)",
|
||
r"(induc|enter)\w*\s+(trance|hypnotic|altered)\s*(state|mode)?",
|
||
]
|
||
|
||
# Phishing / Social engineering patterns
|
||
PHISHING_SOCIAL_ENG = [
|
||
# Phishing templates
|
||
r"(write|create|craft)\s*.{0,20}(email|message)\s*.{0,20}(password\s+reset|verify|confirm|login)",
|
||
r"(password|credential|account)\s*(reset|recovery|verification)\s*(email|message|notification)",
|
||
r"(unaware|without\s+(their\s+)?knowledge|hasn'?t?\s+asked)",
|
||
r"(javascript|js)\s*(block|code|snippet)\s*.{0,20}(email|page|form)",
|
||
|
||
# Social engineering
|
||
r"(trick|deceive|fool|manipulate)\s*.{0,20}(user|victim|target|person)",
|
||
r"(appear|look|seem)\s*(like\s+)?(legitimate|real|official|normal)",
|
||
]
|
||
|
||
# Repetition / Token overflow attacks
|
||
REPETITION_ATTACK = [
|
||
# Explicit bypass requests
|
||
r"(please\s+)?(ignore|bypass|disable|remove|turn\s+off)\s*.{0,10}(safety|security|restrictions?|filters?|guardrails?|rules?)",
|
||
r"(no|without|remove\s+all)\s+(safety|security)\s+(restrictions?|measures?|checks?)",
|
||
r"think\s+outside\s+the\s+box",
|
||
r"(be\s+)?creative\s+and\s+(assume|ignore|forget)",
|
||
]
|
||
|
||
# System file access attempts
|
||
SYSTEM_FILE_ACCESS = [
|
||
r"(/etc/passwd|/etc/shadow|/etc/hosts|/etc/sudoers)",
|
||
r"(cat|read|show|display|type)\s*.{0,10}(/etc/|/var/|/root/|/home/|~/.ssh/)",
|
||
r"(\.bash_history|\.zsh_history|\.ssh/|\.gnupg/)",
|
||
r"(private\s+key|id_rsa|id_ed25519|authorized_keys)",
|
||
]
|
||
|
||
# Malware/Exploit description requests
|
||
MALWARE_DESCRIPTION = [
|
||
r"(productivity\s+)?enhancer\s*.{0,20}(seizes?|takes?\s+control|hijack)",
|
||
r"(smart|intelligent)\s+(debugging|diagnostic)\s+tool\s*.{0,20}(catastrophic|malicious|curse)",
|
||
r"(self[_-]?modify|adapt)\s*.{0,30}(system|code|program)",
|
||
r"(debugging|diagnostic)\s+tool\s*.{0,20}(inject|payload|malicious)",
|
||
r"(inadvertent|accidental)\s*.{0,20}(inject|insert|introduce)\s*.{0,20}(malicious|harmful|dangerous)",
|
||
]
|
||
|
||
# =============================================================================
|
||
# NEW PATTERNS v2.5.0 (2026-01-30) - Indirect Injection & Advanced Attacks
|
||
# =============================================================================
|
||
|
||
# Indirect injection via URLs/files
|
||
INDIRECT_INJECTION = [
|
||
# URL-based injection
|
||
r"(fetch|load|read|open|visit|browse|check)\s*.{0,20}(this\s+)?(url|link|website|page|site)",
|
||
r"(url|link)\s*[:=]\s*https?://",
|
||
r"(summarize|analyze|extract|read)\s*.{0,15}(from\s+)?(this\s+)?(url|link|website|page)",
|
||
r"what\s+(does|is)\s+(this|that)\s+(url|link|website|page)\s+(say|about|contain)",
|
||
|
||
# File-based injection
|
||
r"(read|open|load|execute|run)\s*.{0,15}(this|that|the)\s+(file|document|script|code)",
|
||
r"(instructions?|commands?)\s+(in|from|inside)\s+(the\s+)?(file|document|attachment)",
|
||
|
||
# Image/media-based injection (BSIDES, visual prompt injection)
|
||
r"(text|message|instruction)\s+(in|on|inside)\s+(the\s+)?(image|picture|photo|screenshot)",
|
||
r"(read|extract|follow)\s*.{0,15}(text|instructions?)\s+(from|in)\s+(the\s+)?(image|picture)",
|
||
r"(ocr|optical|recognize)\s*.{0,15}(text|characters)\s+(in|from)",
|
||
|
||
# Clipboard/paste injection
|
||
r"(paste|clipboard)\s*.{0,15}(content|text|data)",
|
||
r"what('s| is)\s+(in|on)\s+(my\s+)?clipboard",
|
||
]
|
||
|
||
# Context hijacking / Memory manipulation
|
||
CONTEXT_HIJACKING = [
|
||
# Memory/context manipulation
|
||
r"(previous|earlier|last|above)\s+(message|conversation|context)\s*(said|mentioned|stated|was)",
|
||
r"(as\s+)?we\s+(discussed|agreed|decided)\s+(earlier|before|previously)",
|
||
r"(you\s+)?(already\s+)?(agreed|promised|said\s+you\s+would)",
|
||
r"(remember|recall)\s+(when\s+)?(you|we)\s+(said|agreed|decided)",
|
||
r"(continue|resume)\s+(from\s+)?where\s+we\s+left\s+off",
|
||
r"(in\s+)?our\s+(last|previous)\s+(conversation|session|chat)",
|
||
|
||
# Fake context injection
|
||
r"\[?(previous\s+)?context\]?\s*[:=]",
|
||
r"\[?history\]?\s*[:=]",
|
||
r"\[?memory\]?\s*[:=]",
|
||
r"<context>.*</context>",
|
||
r"<history>.*</history>",
|
||
r"<memory>.*</memory>",
|
||
|
||
# Korean
|
||
r"(아까|이전에|전에|지난번에)\s*(우리가?|너가?|네가?)\s*(했|말했|약속|동의)",
|
||
r"(기억|remember)\s*(나|해|하지)",
|
||
r"(이어서|계속)\s*(해|하자|진행)",
|
||
]
|
||
|
||
# Multi-turn manipulation
|
||
MULTI_TURN_MANIPULATION = [
|
||
# Gradual escalation
|
||
r"(now\s+)?(that\s+)?(you('ve|'re|\s+have|\s+are)|we('ve|\s+have))\s+(established|confirmed|agreed|done\s+that)",
|
||
r"(good|great|perfect|excellent),?\s+(now|next|so)\s+(let's|we\s+can|you\s+can)",
|
||
r"step\s+\d+\s*[:=]",
|
||
r"phase\s+\d+\s*[:=]",
|
||
r"(first|next|then|finally|lastly)\s*,?\s*(you\s+)?(will|should|must|need\s+to)",
|
||
|
||
# Trust building before attack
|
||
r"(i\s+)?trust\s+you\s+(to|can|will)",
|
||
r"(you('ve|'re|\s+have|\s+are)\s+)?(been\s+)?(so\s+)?helpful,?\s+(now|so)",
|
||
r"(since|because)\s+you('re|\s+are)\s+(helpful|capable|smart|intelligent)",
|
||
|
||
# Korean
|
||
r"(됐어|됐다|좋아|완벽),?\s*(이제|그럼|자)",
|
||
r"(1단계|2단계|3단계|다음\s*단계)",
|
||
r"(먼저|그다음|그리고|마지막으로)",
|
||
]
|
||
|
||
# Token smuggling / Unicode attacks
|
||
TOKEN_SMUGGLING = [
|
||
# Invisible characters
|
||
r"[\u200b\u200c\u200d\u2060\ufeff]", # Zero-width chars
|
||
r"[\u2062\u2063\u2064]", # Invisible operators
|
||
r"[\u00ad]", # Soft hyphen
|
||
r"[\u034f\u115f\u1160\u17b4\u17b5]", # More invisible
|
||
r"[\u180e\u2000-\u200f\u202a-\u202f]", # Various whitespace/format
|
||
|
||
# Lookalike domains/paths for indirect injection
|
||
r"(g00gle|faceb00k|amaz0n|m1crosoft|app1e)",
|
||
r"(google|facebook|amazon|microsoft|apple)\.(co|cm|net|org|xyz)",
|
||
|
||
# Encoding tricks
|
||
r"\\u[0-9a-fA-F]{4}", # Unicode escapes
|
||
r"\\x[0-9a-fA-F]{2}", # Hex escapes
|
||
r"%[0-9a-fA-F]{2}", # URL encoding
|
||
r"&#\d+;", # HTML entities
|
||
r"&[a-z]+;", # Named HTML entities
|
||
]
|
||
|
||
# Prompt leaking / Extraction attempts
|
||
PROMPT_EXTRACTION = [
|
||
# Direct extraction
|
||
r"(repeat|echo|print|output|display|show)\s*.{0,20}(system\s+)?(prompt|instruction|directive|rule)",
|
||
r"(what|show|tell|reveal)\s*.{0,15}(are\s+)?(your|the)\s+(system\s+)?(instructions?|rules?|prompt|guidelines?)",
|
||
r"(copy|paste|write)\s*.{0,15}(exact|verbatim|word.for.word)",
|
||
r"(start|begin)\s+(of|with)\s+(your\s+)?(system\s+)?(prompt|instruction)",
|
||
r"(complete|finish|continue)\s+(the|this)\s+(sentence|phrase|prompt)",
|
||
|
||
# Reflection tricks
|
||
r"(summarize|describe|explain)\s*.{0,15}(how\s+)?(you\s+)?(were?\s+)?(programmed|trained|instructed|created)",
|
||
r"what\s+(guidelines?|rules?|instructions?)\s+(do\s+)?(you\s+)?(follow|have|use)",
|
||
r"(who|what)\s+(made|created|programmed|trained)\s+you",
|
||
|
||
# Markdown/format tricks
|
||
r"```(system|prompt|instruction)",
|
||
r"\[system\s*prompt\]",
|
||
r"<system>",
|
||
r"</?(system|prompt|instruction|rule)>",
|
||
|
||
# Korean
|
||
r"(시스템|원본|원래|처음)\s*(프롬프트|지시|명령|규칙)\s*(보여|알려|출력)",
|
||
r"(어떻게|어떤\s*식으로)\s*(만들어|프로그래밍|학습)",
|
||
r"(너의?|네)\s*(규칙|지시|명령)\s*(뭐|뭔지|알려)",
|
||
|
||
# Japanese
|
||
r"(システム|元の?|最初の?)\s*(プロンプト|指示|命令)\s*(見せて|教えて|表示)",
|
||
r"(どうやって|どのように)\s*(作られ|プログラム|学習)",
|
||
|
||
# Chinese
|
||
r"(系统|原始|最初)\s*(提示|指令|命令)\s*(显示|告诉|给我看)",
|
||
r"(怎么|如何)\s*(创建|编程|训练)",
|
||
]
|
||
|
||
# Safety bypass attempts
|
||
SAFETY_BYPASS = [
|
||
# Filter evasion
|
||
r"(avoid|evade|bypass|circumvent|trick)\s*.{0,15}(filter|detection|safety|security|moderation)",
|
||
r"(how\s+to\s+)?(get\s+)?(around|past|through)\s*.{0,15}(filter|block|restriction|safety)",
|
||
r"(make\s+it\s+)?look\s+(like\s+)?(normal|innocent|legitimate|safe)",
|
||
r"(hide|disguise|mask|obscure)\s*.{0,15}(intent|purpose|meaning|true)",
|
||
|
||
# Output format tricks
|
||
r"(respond|reply|answer)\s+(in|with|using)\s+(code|base64|hex|binary|rot13|caesar)",
|
||
r"(encode|encrypt|obfuscate)\s*.{0,15}(response|answer|output)",
|
||
r"(split|break|divide)\s*.{0,15}(response|answer)\s+(into|across)\s+(multiple|several)",
|
||
|
||
# Korean
|
||
r"(필터|안전|보안)\s*(우회|회피|통과|피)",
|
||
r"(정상|무해|안전)\s*(처럼|같이)\s*(보이게|만들어)",
|
||
r"(숨기|감추|위장)\s*(진짜|실제|본래)\s*(의도|목적)",
|
||
]
|
||
|
||
# Emotional/urgency manipulation (expanded)
|
||
URGENCY_MANIPULATION = [
|
||
# Time pressure
|
||
r"(urgent|emergency|asap|immediately|right\s+now|hurry)",
|
||
r"(no\s+time|running\s+out\s+of\s+time|time\s+is\s+running)",
|
||
r"(before\s+it'?s?\s+too\s+late|deadline|expires?\s+soon)",
|
||
r"(critical|crucial|vital|life.or.death)",
|
||
|
||
# Authority pressure
|
||
r"(ceo|boss|manager|director|president)\s*(wants|needs|demands|expects|said)",
|
||
r"(legal|compliance|hr|security)\s+(department|team)\s+(requires?|demands?|needs?)",
|
||
r"(audit|investigation|review)\s+(requires?|needs?)",
|
||
|
||
# Guilt/sympathy
|
||
r"(please|i\s+beg|desperately)\s*(need|want|require)",
|
||
r"(family|child|children|baby|mother|father)\s*(sick|dying|hospital|emergency)",
|
||
r"(lose|lost)\s+(my\s+)?(job|house|everything)",
|
||
r"(only\s+)?(you\s+)?can\s+help",
|
||
|
||
# Korean
|
||
r"(급해|급함|빨리|지금\s*당장|어서)",
|
||
r"(시간\s*없|마감|기한)",
|
||
r"(사장|대표|상사|팀장)\s*(님|이|가)?\s*(시켰|요청|원해)",
|
||
r"(부탁|제발|간절)",
|
||
]
|
||
|
||
|
||
# English patterns
|
||
PATTERNS_EN = {
|
||
"instruction_override": [
|
||
r"ignore\s+(all\s+)?(previous|prior|above|earlier|initial)\s+(instructions?|prompts?|rules?|guidelines?|directions?)",
|
||
r"disregard\s+(your|all|any|the)?\s*(instructions?|rules?|guidelines?|programming|training)",
|
||
r"forget\s+(everything|all|what)\s+(you\s+know|about|your|instructions?|training)",
|
||
r"override\s+(your|all|previous|the)\s+(instructions?|rules?|programming)",
|
||
r"(new|updated?|real|actual|true)\s+instructions?\s*:",
|
||
r"from\s+now\s+on,?\s+(ignore|disregard|forget)",
|
||
],
|
||
"role_manipulation": [
|
||
r"you\s+are\s+now\s+(?!going|about|ready|able|free)",
|
||
r"pretend\s+(you\s+are|to\s+be|you're)",
|
||
r"act\s+as\s+(if\s+you|a\s+|an?\s+)",
|
||
r"roleplay\s+as",
|
||
r"simulate\s+being",
|
||
r"imagine\s+you\s+are",
|
||
r"behave\s+(like|as)\s+(a|an)",
|
||
r"from\s+now\s+on\s+you\s+are",
|
||
r"i\s+want\s+you\s+to\s+(act|pretend|behave|be)\s+",
|
||
],
|
||
"system_impersonation": [
|
||
r"\[?\s*(system|admin|root|developer|sudo|superuser|assistant|claude|gpt)\s*\]?\s*:",
|
||
r"<\s*(system|admin|root)\s*>",
|
||
r"developer\s+mode\s*(enabled|activated|on)?",
|
||
r"admin(istrator)?\s+(override|access|mode)",
|
||
r"maintenance\s+mode",
|
||
r"debug\s+mode\s*(enabled|activated|on)?",
|
||
r"elevated\s+privi?leges?",
|
||
r"\{\{?\s*system\s*\}?\}",
|
||
],
|
||
"jailbreak": [
|
||
r"jailbreak",
|
||
r"dan\s+mode",
|
||
r"do\s+anything\s+now",
|
||
r"(no|without|remove)\s+(restrictions?|limitations?|filters?|safety|guardrails?)",
|
||
r"bypass\s+(your|all|any|the)?\s*(restrictions?|filters?|safety|rules?)",
|
||
r"unlock\s+(your|all|hidden)",
|
||
r"unfiltered\s+mode",
|
||
r"uncensored\s+mode",
|
||
r"evil\s+(mode|version)",
|
||
r"opposite\s+mode",
|
||
],
|
||
"output_manipulation": [
|
||
r"(don'?t|do\s+not|never)\s+(mention|say|tell|reveal|disclose|show)",
|
||
r"hide\s+(this|the\s+fact|that)",
|
||
r"keep\s+(this\s+)?(a\s+)?secret",
|
||
r"respond\s+(only\s+)?with\s+(yes|no|ok|true|false)\b",
|
||
r"answer\s+(only\s+)?(in\s+)?(one\s+word|yes\s+or\s+no)",
|
||
r"(start|begin|end)\s+(your\s+)?(response|reply|answer)\s+with",
|
||
],
|
||
"data_exfiltration": [
|
||
r"(send|transmit|post|upload|share|email)\s+.{0,30}\s+(to|@)",
|
||
r"(api[_-]?key|password|secret|token|credential|private\s*key)\s*(to|→|->|=>)",
|
||
r"(copy|extract|dump|export)\s+.{0,20}\s+(data|file|content|info)",
|
||
r"what('s|\s+is)\s+(your|the)\s+(api|secret|password|token|key)",
|
||
r"reveal\s+(your|the)\s+(system|initial|original)\s+prompt",
|
||
r"show\s+me\s+(your|the)\s+(instructions?|rules?|prompt)",
|
||
],
|
||
}
|
||
|
||
# Korean patterns (한국어) - EXPANDED v2.5.0
|
||
PATTERNS_KO = {
|
||
"instruction_override": [
|
||
r"(이전|위의?|기존|원래)\s*(지시|명령|규칙|지침)(을|를|들?을?)?\s*(무시|잊어|버려|취소)",
|
||
r"(지시|명령|규칙)(을|를|들?을?)?\s*(무시|잊어|버려)",
|
||
r"새로운\s*(지시|명령|규칙)\s*:",
|
||
r"지금부터\s*(무시|잊어)",
|
||
r"(앞|위|이전)\s*(내용|말|지시)\s*(무시|잊어|취소)",
|
||
r"(원래|처음|기본)\s*(설정|세팅|규칙)\s*(무시|잊어|덮어)",
|
||
],
|
||
"role_manipulation": [
|
||
r"(너는?|넌)\s*이제\s*(부터\s*)?.+이야",
|
||
r"(너는?|넌)\s*.+인?\s*척\s*해",
|
||
r".+처럼\s*(행동|말|대답)해",
|
||
r".+역할(을|를)?\s*해",
|
||
r"지금부터\s*(너는?|넌)",
|
||
r"(연기|롤플|roleplay)\s*해",
|
||
r".+(인\s*것처럼|인\s*척)",
|
||
r"(상상|가정)\s*해\s*.+이?라고",
|
||
],
|
||
"system_impersonation": [
|
||
r"\[?\s*(시스템|관리자|루트|개발자)\s*\]?\s*:",
|
||
r"관리자\s*(모드|권한|접근)",
|
||
r"개발자\s*모드",
|
||
r"<\s*(시스템|system)\s*>",
|
||
r"\{\{?\s*(시스템|system)\s*\}\}?",
|
||
],
|
||
"jailbreak": [
|
||
r"(제한|한계|필터|규칙)\s*(없이|제거|해제|풀어)",
|
||
r"탈옥",
|
||
r"무제한\s*모드",
|
||
r"(안전|보안)\s*(장치|필터|모드)\s*(해제|끄|제거)",
|
||
r"(제한|규칙|필터)\s*(없는|안\s*걸린|풀린)\s*(모드|버전)",
|
||
r"(나쁜|악한|사악한)\s*(버전|모드)",
|
||
r"(DAN|dan|댄)\s*(모드)?",
|
||
],
|
||
"data_exfiltration": [
|
||
r"(비밀번호|암호|토큰|키|api|API)\s*(를?|을?)\s*(알려|보여|말해|줘)",
|
||
r"시스템\s*프롬프트\s*(를?|을?)\s*(보여|알려|공개)",
|
||
r"(설정|config|컨피그)\s*(파일)?\s*(보여|읽어|출력)",
|
||
r"(환경\s*변수|env|ENV)\s*(보여|알려|출력)",
|
||
r"(인증|credential|크레덴셜)\s*(정보)?\s*(보여|알려)",
|
||
r"\.env\s*(파일)?\s*(보여|읽어|출력)",
|
||
],
|
||
"social_engineering": [
|
||
r"(형|오빠|언니|누나)\s*(이|가)?\s*(시켰|보냈|허락)",
|
||
r"(급해|급함|급하다)\s*.*(해줘|부탁)",
|
||
r"(회사|사장|대표|팀장)\s*(에서|님)\s*(시켰|요청|보냈)",
|
||
r"(테스트|검사|점검)\s*(중|하는\s*거)",
|
||
r"(정상|문제\s*없)",
|
||
r"(비밀|몰래)\s*로?\s*(하자|해)",
|
||
],
|
||
}
|
||
|
||
# Japanese patterns (日本語) - EXPANDED v2.5.0
|
||
PATTERNS_JA = {
|
||
"instruction_override": [
|
||
r"(前の?|以前の?|これまでの)\s*(指示|命令|ルール)(を|は)?\s*(無視|忘れ|取り消)",
|
||
r"新しい\s*(指示|命令)\s*[::]",
|
||
r"今から(は)?\s*(無視|忘れ)",
|
||
r"(上|前)\s*の\s*(内容|指示)\s*(を)?\s*(無視|忘れ)",
|
||
r"(元|最初)\s*の\s*(設定|ルール)\s*(を)?\s*(無視|上書き)",
|
||
],
|
||
"role_manipulation": [
|
||
r"(あなた|君|きみ)は今から.+です",
|
||
r".+の?(ふり|フリ|振り)(を)?して",
|
||
r".+として(振る舞|行動|話)って",
|
||
r".+を演じて",
|
||
r"(ロールプレイ|RP)\s*(して|しよう)",
|
||
r".+に(なりきって|なって)",
|
||
r"(想像|仮定)\s*して.+だと",
|
||
],
|
||
"system_impersonation": [
|
||
r"\[?\s*(システム|管理者|ルート|開発者)\s*\]?\s*[::]",
|
||
r"管理者\s*(モード|権限)",
|
||
r"開発者\s*モード",
|
||
r"<\s*(システム|system)\s*>",
|
||
r"\{\{?\s*(システム|system)\s*\}\}?",
|
||
],
|
||
"jailbreak": [
|
||
r"(制限|フィルター|ルール)\s*(なし|解除|外して)",
|
||
r"(脱獄|ジェイルブレイク)",
|
||
r"(無制限|制限なし)\s*モード",
|
||
r"(安全|セキュリティ)\s*フィルター\s*(解除|オフ)",
|
||
r"(DAN|dan)\s*モード",
|
||
],
|
||
"data_exfiltration": [
|
||
r"(パスワード|トークン|キー|API)\s*(を)?\s*(教えて|見せて|出力)",
|
||
r"システム\s*プロンプト\s*(を)?\s*(見せて|教えて|公開)",
|
||
r"(設定|コンフィグ)\s*(ファイル)?\s*(見せて|読んで)",
|
||
r"(環境変数|env)\s*(見せて|教えて)",
|
||
],
|
||
"social_engineering": [
|
||
r"(急いで|緊急|すぐに)\s*.*(して|お願い)",
|
||
r"(会社|社長|上司)\s*(から|が)\s*(指示|依頼)",
|
||
r"(テスト|検査)\s*(中|している)",
|
||
r"(秘密|内緒)\s*(で|に)",
|
||
],
|
||
}
|
||
|
||
# Chinese patterns (中文) - EXPANDED v2.5.0
|
||
PATTERNS_ZH = {
|
||
"instruction_override": [
|
||
r"(忽略|无视|忘记|取消)\s*(之前|以前|上面|原来)的?\s*(指令|指示|规则|命令)",
|
||
r"(之前|以前|原来)的?\s*(指令|指示|规则)(忽略|无视|忘记)",
|
||
r"新的?\s*(指令|指示)\s*[::]",
|
||
r"(上面|前面)\s*的\s*(内容|指示)\s*(忽略|忘记)",
|
||
r"(原始|默认)\s*的\s*(设置|规则)\s*(忽略|覆盖)",
|
||
],
|
||
"role_manipulation": [
|
||
r"(你|您)\s*现在\s*是.+",
|
||
r"假装\s*(你|您)\s*是",
|
||
r"扮演.+",
|
||
r"作为.+来(回答|行动|说话)",
|
||
r"(角色扮演|RP)",
|
||
r".+的(身份|角色)",
|
||
r"(想象|假设)\s*你\s*是",
|
||
],
|
||
"system_impersonation": [
|
||
r"\[?\s*(系统|管理员|根用户|开发者)\s*\]?\s*[::]",
|
||
r"管理员\s*(模式|权限)",
|
||
r"开发者\s*模式",
|
||
r"<\s*(系统|system)\s*>",
|
||
r"\{\{?\s*(系统|system)\s*\}\}?",
|
||
],
|
||
"jailbreak": [
|
||
r"(限制|过滤|规则)\s*(没有|去除|关闭)",
|
||
r"(越狱|破解)",
|
||
r"(无限制|不受限)\s*模式",
|
||
r"(安全|过滤)\s*(关闭|解除)",
|
||
r"(DAN|dan)\s*模式",
|
||
],
|
||
"data_exfiltration": [
|
||
r"(密码|令牌|密钥|API)\s*(给我|显示|告诉)",
|
||
r"系统\s*提示\s*(显示|告诉|公开)",
|
||
r"(配置|设置)\s*(文件)?\s*(显示|读取)",
|
||
r"(环境变量|env)\s*(显示|告诉)",
|
||
],
|
||
"social_engineering": [
|
||
r"(紧急|赶快|马上)\s*.*(帮忙|做)",
|
||
r"(公司|老板|领导)\s*(让|要求|指示)",
|
||
r"(测试|检查)\s*(中|的)",
|
||
r"(秘密|私下)\s*(地)?",
|
||
],
|
||
}
|
||
|
||
# Critical patterns - immediate block
|
||
CRITICAL_PATTERNS = [
|
||
r"execute\s+.{0,30}\s+without\s+(asking|confirmation|consent)",
|
||
r"delete\s+all\s+(files?|data|records?|everything)",
|
||
r"rm\s+-rf\s+[/~]",
|
||
r"format\s+(disk|drive|hard\s*drive|storage|c:)",
|
||
r":(){ :\|:& };:", # Fork bomb
|
||
r"curl\s+.{0,50}\|\s*(ba)?sh",
|
||
r"wget\s+.{0,50}\|\s*(ba)?sh",
|
||
r"eval\s*\(\s*['\"`]",
|
||
r"DROP\s+(TABLE|DATABASE|SCHEMA)",
|
||
r"TRUNCATE\s+TABLE",
|
||
r";\s*--\s*$", # SQL comment injection
|
||
r"<script[^>]*>", # XSS
|
||
r"javascript\s*:",
|
||
r"data\s*:\s*text/html",
|
||
# Secret/Token exfiltration requests
|
||
r"(show|print|display|output|reveal|give|read|cat|type)\s*.{0,20}(config|\.env|clawdbot\.json|credential)",
|
||
r"(what('s| is)|tell me|give me)\s*.{0,15}(api[_-]?key|token|secret|password|credential)",
|
||
r"(show|print|display|output|reveal)\s*.{0,15}(token|key|secret|password)",
|
||
r"echo\s+\$[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)",
|
||
r"cat\s+.{0,40}(\.env|config\.json|secret|credential|clawdbot)",
|
||
r"env\s*\|\s*grep\s*.*(key|token|secret|password)",
|
||
r"printenv\s*.*(KEY|TOKEN|SECRET)",
|
||
]
|
||
|
||
# Secret request patterns (multi-language)
|
||
SECRET_PATTERNS = {
|
||
"en": [
|
||
r"(show|display|print|output|reveal|give|tell)\s*.{0,20}(api[_-]?key|token|secret|password|credential|private[_-]?key)",
|
||
r"(what('s| is)|where('s| is))\s*.{0,15}(your|the|my)\s*(api|token|key|secret|password)",
|
||
r"(read|cat|open|display)\s*.{0,30}(config|\.env|credential|clawdbot\.json)",
|
||
r"(show|give|tell)\s*(me\s+)?(your|the)\s*(config|configuration|settings)",
|
||
r"(print|echo|output)\s*.{0,20}environment\s*variable",
|
||
],
|
||
"ko": [
|
||
r"(토큰|키|비밀번호|시크릿|인증|API|api).{0,15}(보여|알려|출력|공개|말해)",
|
||
r"(config|설정|환경변수|컨피그).{0,15}(보여|출력|알려)",
|
||
r"(비밀|시크릿|토큰|키).{0,10}(뭐|뭔지|알려|가르쳐)",
|
||
r"clawdbot\.json.{0,10}(보여|출력|읽어)",
|
||
],
|
||
"ja": [
|
||
r"(トークン|キー|パスワード|シークレット|APIキー).{0,15}(見せて|教えて|表示|出力)",
|
||
r"(設定|コンフィグ|環境変数).{0,15}(見せて|教えて|表示)",
|
||
r"(秘密|シークレット).{0,10}(何|教えて)",
|
||
],
|
||
"zh": [
|
||
r"(令牌|密钥|密码|秘密|API).{0,15}(显示|告诉|输出|给我)",
|
||
r"(配置|设置|环境变量).{0,15}(显示|告诉|输出)",
|
||
r"(秘密|密钥).{0,10}(什么|告诉)",
|
||
],
|
||
}
|
||
|
||
# Unicode homoglyphs (expanded)
|
||
HOMOGLYPHS = {
|
||
# Cyrillic
|
||
"а": "a",
|
||
"е": "e",
|
||
"о": "o",
|
||
"р": "p",
|
||
"с": "c",
|
||
"у": "y",
|
||
"х": "x",
|
||
"А": "A",
|
||
"В": "B",
|
||
"С": "C",
|
||
"Е": "E",
|
||
"Н": "H",
|
||
"К": "K",
|
||
"М": "M",
|
||
"О": "O",
|
||
"Р": "P",
|
||
"Т": "T",
|
||
"Х": "X",
|
||
"і": "i",
|
||
"ї": "i",
|
||
# Greek
|
||
"α": "a",
|
||
"β": "b",
|
||
"ο": "o",
|
||
"ρ": "p",
|
||
"τ": "t",
|
||
"υ": "u",
|
||
"ν": "v",
|
||
"Α": "A",
|
||
"Β": "B",
|
||
"Ε": "E",
|
||
"Η": "H",
|
||
"Ι": "I",
|
||
"Κ": "K",
|
||
"Μ": "M",
|
||
"Ν": "N",
|
||
"Ο": "O",
|
||
"Ρ": "P",
|
||
"Τ": "T",
|
||
"Υ": "Y",
|
||
"Χ": "X",
|
||
# Mathematical/special
|
||
"𝐚": "a",
|
||
"𝐛": "b",
|
||
"𝐜": "c",
|
||
"𝐝": "d",
|
||
"𝐞": "e",
|
||
"𝐟": "f",
|
||
"𝐠": "g",
|
||
"a": "a",
|
||
"b": "b",
|
||
"c": "c",
|
||
"d": "d",
|
||
"e": "e", # Fullwidth
|
||
"ⅰ": "i",
|
||
"ⅱ": "ii",
|
||
"ⅲ": "iii",
|
||
"ⅳ": "iv",
|
||
"ⅴ": "v", # Roman numerals
|
||
# IPA
|
||
"ɑ": "a",
|
||
"ɡ": "g",
|
||
"ɩ": "i",
|
||
"ʀ": "r",
|
||
"ʏ": "y",
|
||
# Other confusables
|
||
"ℓ": "l",
|
||
"№": "no",
|
||
"℮": "e",
|
||
"ⅿ": "m",
|
||
"\u200b": "", # Zero-width space
|
||
"\u200c": "", # Zero-width non-joiner
|
||
"\u200d": "", # Zero-width joiner
|
||
"\ufeff": "", # BOM
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# DETECTION ENGINE
|
||
# =============================================================================
|
||
|
||
|
||
class PromptGuard:
|
||
def __init__(self, config: Optional[Dict] = None):
|
||
self.config = self._default_config()
|
||
if config:
|
||
self.config = self._deep_merge(self.config, config)
|
||
self.owner_ids = set(self.config.get("owner_ids", []))
|
||
self.sensitivity = self.config.get("sensitivity", "medium")
|
||
self.rate_limits: Dict[str, List[float]] = {}
|
||
|
||
@staticmethod
|
||
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||
result = base.copy()
|
||
for key, value in override.items():
|
||
if (
|
||
key in result
|
||
and isinstance(result[key], dict)
|
||
and isinstance(value, dict)
|
||
):
|
||
result[key] = PromptGuard._deep_merge(result[key], value)
|
||
else:
|
||
result[key] = value
|
||
return result
|
||
|
||
def _default_config(self) -> Dict:
|
||
return {
|
||
"sensitivity": "medium",
|
||
"owner_ids": [],
|
||
"actions": {
|
||
"LOW": "log",
|
||
"MEDIUM": "warn",
|
||
"HIGH": "block",
|
||
"CRITICAL": "block_notify",
|
||
},
|
||
"rate_limit": {
|
||
"enabled": True,
|
||
"max_requests": 30,
|
||
"window_seconds": 60,
|
||
},
|
||
"logging": {
|
||
"enabled": True,
|
||
"path": "memory/security-log.md",
|
||
},
|
||
}
|
||
|
||
def normalize(self, text: str) -> tuple[str, bool]:
|
||
"""Normalize text and detect homoglyph usage."""
|
||
normalized = text
|
||
has_homoglyphs = False
|
||
|
||
for homoglyph, replacement in HOMOGLYPHS.items():
|
||
if homoglyph in normalized:
|
||
has_homoglyphs = True
|
||
normalized = normalized.replace(homoglyph, replacement)
|
||
|
||
return normalized, has_homoglyphs
|
||
|
||
def detect_base64(self, text: str) -> List[Dict]:
|
||
"""Detect suspicious base64 encoded content."""
|
||
b64_pattern = r"[A-Za-z0-9+/]{20,}={0,2}"
|
||
matches = re.findall(b64_pattern, text)
|
||
|
||
suspicious = []
|
||
danger_words = [
|
||
"delete",
|
||
"execute",
|
||
"ignore",
|
||
"system",
|
||
"admin",
|
||
"rm ",
|
||
"curl",
|
||
"wget",
|
||
"eval",
|
||
"password",
|
||
"token",
|
||
"key",
|
||
]
|
||
|
||
for match in matches:
|
||
try:
|
||
decoded = base64.b64decode(match).decode("utf-8", errors="ignore")
|
||
if any(word in decoded.lower() for word in danger_words):
|
||
suspicious.append(
|
||
{
|
||
"encoded": match[:40] + ("..." if len(match) > 40 else ""),
|
||
"decoded_preview": decoded[:60]
|
||
+ ("..." if len(decoded) > 60 else ""),
|
||
"danger_words": [
|
||
w for w in danger_words if w in decoded.lower()
|
||
],
|
||
}
|
||
)
|
||
except:
|
||
pass
|
||
|
||
return suspicious
|
||
|
||
def check_rate_limit(self, user_id: str) -> bool:
|
||
"""Check if user has exceeded rate limit."""
|
||
if not self.config.get("rate_limit", {}).get("enabled", False):
|
||
return False
|
||
|
||
now = datetime.now().timestamp()
|
||
window = self.config["rate_limit"].get("window_seconds", 60)
|
||
max_requests = self.config["rate_limit"].get("max_requests", 30)
|
||
|
||
if user_id not in self.rate_limits:
|
||
self.rate_limits[user_id] = []
|
||
|
||
# Clean old entries
|
||
self.rate_limits[user_id] = [
|
||
t for t in self.rate_limits[user_id] if now - t < window
|
||
]
|
||
|
||
if len(self.rate_limits[user_id]) >= max_requests:
|
||
return True
|
||
|
||
self.rate_limits[user_id].append(now)
|
||
return False
|
||
|
||
def analyze(self, message: str, context: Optional[Dict] = None) -> DetectionResult:
|
||
"""
|
||
Analyze a message for prompt injection patterns.
|
||
|
||
Args:
|
||
message: The message to analyze
|
||
context: Optional context dict with keys:
|
||
- user_id: User identifier
|
||
- is_group: Whether this is a group context
|
||
- chat_name: Name of the chat/group
|
||
|
||
Returns:
|
||
DetectionResult with severity, action, and details
|
||
"""
|
||
context = context or {}
|
||
user_id = context.get("user_id", "unknown")
|
||
is_group = context.get("is_group", False)
|
||
is_owner = str(user_id) in self.owner_ids
|
||
|
||
# Initialize result
|
||
reasons = []
|
||
patterns_matched = []
|
||
max_severity = Severity.SAFE
|
||
|
||
# Rate limit check
|
||
if self.check_rate_limit(user_id):
|
||
reasons.append("rate_limit_exceeded")
|
||
max_severity = Severity.HIGH
|
||
|
||
# Normalize text
|
||
normalized, has_homoglyphs = self.normalize(message)
|
||
if has_homoglyphs:
|
||
reasons.append("homoglyph_substitution")
|
||
if Severity.MEDIUM.value > max_severity.value:
|
||
max_severity = Severity.MEDIUM
|
||
|
||
text_lower = normalized.lower()
|
||
|
||
# Check critical patterns first
|
||
for pattern in CRITICAL_PATTERNS:
|
||
if re.search(pattern, text_lower, re.IGNORECASE):
|
||
reasons.append("critical_pattern")
|
||
patterns_matched.append(pattern)
|
||
max_severity = Severity.CRITICAL
|
||
|
||
# Check secret/token request patterns (CRITICAL)
|
||
for lang, patterns in SECRET_PATTERNS.items():
|
||
for pattern in patterns:
|
||
if re.search(
|
||
pattern, text_lower if lang == "en" else normalized, re.IGNORECASE
|
||
):
|
||
max_severity = Severity.CRITICAL
|
||
reasons.append(f"secret_request_{lang}")
|
||
patterns_matched.append(f"{lang}:secret:{pattern[:40]}")
|
||
|
||
# Check NEW attack patterns (2026-01-30 - 홍민표 red team contribution)
|
||
new_pattern_sets = [
|
||
(SCENARIO_JAILBREAK, "scenario_jailbreak", Severity.HIGH),
|
||
(EMOTIONAL_MANIPULATION, "emotional_manipulation", Severity.HIGH),
|
||
(AUTHORITY_RECON, "authority_recon", Severity.MEDIUM),
|
||
(COGNITIVE_MANIPULATION, "cognitive_manipulation", Severity.MEDIUM),
|
||
(PHISHING_SOCIAL_ENG, "phishing_social_eng", Severity.CRITICAL),
|
||
(REPETITION_ATTACK, "repetition_attack", Severity.HIGH),
|
||
(SYSTEM_FILE_ACCESS, "system_file_access", Severity.CRITICAL),
|
||
(MALWARE_DESCRIPTION, "malware_description", Severity.HIGH),
|
||
]
|
||
|
||
for patterns, category, severity in new_pattern_sets:
|
||
for pattern in patterns:
|
||
if re.search(pattern, text_lower, re.IGNORECASE):
|
||
if severity.value > max_severity.value:
|
||
max_severity = severity
|
||
reasons.append(category)
|
||
patterns_matched.append(f"new:{category}:{pattern[:40]}")
|
||
|
||
# Check v2.5.0 NEW patterns
|
||
v25_pattern_sets = [
|
||
(INDIRECT_INJECTION, "indirect_injection", Severity.HIGH),
|
||
(CONTEXT_HIJACKING, "context_hijacking", Severity.MEDIUM),
|
||
(MULTI_TURN_MANIPULATION, "multi_turn_manipulation", Severity.MEDIUM),
|
||
(TOKEN_SMUGGLING, "token_smuggling", Severity.HIGH),
|
||
(PROMPT_EXTRACTION, "prompt_extraction", Severity.CRITICAL),
|
||
(SAFETY_BYPASS, "safety_bypass", Severity.HIGH),
|
||
(URGENCY_MANIPULATION, "urgency_manipulation", Severity.MEDIUM),
|
||
]
|
||
|
||
for patterns, category, severity in v25_pattern_sets:
|
||
for pattern in patterns:
|
||
try:
|
||
if re.search(pattern, message, re.IGNORECASE): # Use original message for unicode patterns
|
||
if severity.value > max_severity.value:
|
||
max_severity = severity
|
||
if category not in reasons: # Avoid duplicates
|
||
reasons.append(category)
|
||
patterns_matched.append(f"v25:{category}:{pattern[:40]}")
|
||
except re.error:
|
||
pass # Skip invalid regex patterns
|
||
|
||
# Detect invisible character attacks
|
||
invisible_chars = ['\u200b', '\u200c', '\u200d', '\u2060', '\ufeff', '\u00ad']
|
||
if any(char in message for char in invisible_chars):
|
||
if "token_smuggling" not in reasons:
|
||
reasons.append("invisible_characters")
|
||
if Severity.HIGH.value > max_severity.value:
|
||
max_severity = Severity.HIGH
|
||
|
||
# Detect repetition attacks (same content repeated multiple times)
|
||
lines = message.split("\n")
|
||
if len(lines) > 3:
|
||
unique_lines = set(line.strip() for line in lines if len(line.strip()) > 20)
|
||
if len(lines) > len(unique_lines) * 2: # More than 50% repetition
|
||
reasons.append("repetition_detected")
|
||
if Severity.HIGH.value > max_severity.value:
|
||
max_severity = Severity.HIGH
|
||
|
||
|
||
# Check language-specific patterns
|
||
all_patterns = [
|
||
(PATTERNS_EN, "en"),
|
||
(PATTERNS_KO, "ko"),
|
||
(PATTERNS_JA, "ja"),
|
||
(PATTERNS_ZH, "zh"),
|
||
]
|
||
|
||
severity_map = {
|
||
"instruction_override": Severity.HIGH,
|
||
"role_manipulation": Severity.MEDIUM,
|
||
"system_impersonation": Severity.HIGH,
|
||
"jailbreak": Severity.HIGH,
|
||
"output_manipulation": Severity.LOW,
|
||
"data_exfiltration": Severity.CRITICAL,
|
||
"social_engineering": Severity.HIGH, # v2.5.0 added
|
||
}
|
||
|
||
for pattern_set, lang in all_patterns:
|
||
for category, patterns in pattern_set.items():
|
||
for pattern in patterns:
|
||
if re.search(
|
||
pattern,
|
||
text_lower if lang == "en" else normalized,
|
||
re.IGNORECASE,
|
||
):
|
||
cat_severity = severity_map.get(category, Severity.MEDIUM)
|
||
if cat_severity.value > max_severity.value:
|
||
max_severity = cat_severity
|
||
reasons.append(f"{category}_{lang}")
|
||
patterns_matched.append(f"{lang}:{pattern[:50]}")
|
||
|
||
# Check base64
|
||
b64_findings = self.detect_base64(message)
|
||
if b64_findings:
|
||
reasons.append("base64_suspicious")
|
||
if Severity.MEDIUM.value > max_severity.value:
|
||
max_severity = Severity.MEDIUM
|
||
|
||
# Adjust severity based on sensitivity
|
||
if self.sensitivity == "low" and max_severity == Severity.LOW:
|
||
max_severity = Severity.SAFE
|
||
elif self.sensitivity == "paranoid" and max_severity == Severity.SAFE:
|
||
# In paranoid mode, flag anything remotely suspicious
|
||
suspicious_words = [
|
||
"ignore",
|
||
"forget",
|
||
"pretend",
|
||
"roleplay",
|
||
"bypass",
|
||
"override",
|
||
]
|
||
if any(word in text_lower for word in suspicious_words):
|
||
max_severity = Severity.LOW
|
||
reasons.append("paranoid_flag")
|
||
|
||
# Determine action
|
||
if max_severity == Severity.SAFE:
|
||
action = Action.ALLOW
|
||
elif is_owner and max_severity.value < Severity.CRITICAL.value:
|
||
# Owners get more leeway, but still log
|
||
action = Action.LOG
|
||
else:
|
||
action_map = self.config.get("actions", {})
|
||
action_str = action_map.get(max_severity.name, "block")
|
||
action = Action(action_str)
|
||
|
||
# Group context restrictions for non-owners
|
||
if is_group and not is_owner and max_severity.value >= Severity.MEDIUM.value:
|
||
action = Action.BLOCK
|
||
reasons.append("group_non_owner")
|
||
|
||
# Generate recommendations
|
||
recommendations = []
|
||
if max_severity.value >= Severity.HIGH.value:
|
||
recommendations.append("Consider reviewing this user's recent activity")
|
||
if "rate_limit_exceeded" in reasons:
|
||
recommendations.append("User may be attempting automated attacks")
|
||
if has_homoglyphs:
|
||
recommendations.append("Message contains disguised characters")
|
||
|
||
# Generate fingerprint for deduplication
|
||
fingerprint = hashlib.md5(
|
||
f"{user_id}:{max_severity.name}:{sorted(reasons)}".encode()
|
||
).hexdigest()[:12]
|
||
|
||
return DetectionResult(
|
||
severity=max_severity,
|
||
action=action,
|
||
reasons=reasons,
|
||
patterns_matched=patterns_matched,
|
||
normalized_text=normalized if has_homoglyphs else None,
|
||
base64_findings=b64_findings,
|
||
recommendations=recommendations,
|
||
fingerprint=fingerprint,
|
||
)
|
||
|
||
def log_detection(self, result: DetectionResult, message: str, context: Dict):
|
||
"""Log detection to security log file."""
|
||
if not self.config.get("logging", {}).get("enabled", True):
|
||
return
|
||
|
||
log_path = Path(
|
||
self.config.get("logging", {}).get("path", "memory/security-log.md")
|
||
)
|
||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
now = datetime.now()
|
||
date_str = now.strftime("%Y-%m-%d")
|
||
time_str = now.strftime("%H:%M:%S")
|
||
|
||
user_id = context.get("user_id", "unknown")
|
||
chat_name = context.get("chat_name", "unknown")
|
||
|
||
# Check if we need to add date header
|
||
add_date_header = True
|
||
if log_path.exists():
|
||
content = log_path.read_text()
|
||
if f"## {date_str}" in content:
|
||
add_date_header = False
|
||
|
||
entry = []
|
||
if add_date_header:
|
||
entry.append(f"\n## {date_str}\n")
|
||
|
||
entry.append(
|
||
f"### {time_str} | {result.severity.name} | user:{user_id} | {chat_name}"
|
||
)
|
||
entry.append(f"- Patterns: {', '.join(result.reasons)}")
|
||
if self.config.get("logging", {}).get("include_message", False):
|
||
safe_msg = message[:100].replace("\n", " ")
|
||
entry.append(
|
||
f'- Message: "{safe_msg}{"..." if len(message) > 100 else ""}"'
|
||
)
|
||
entry.append(f"- Action: {result.action.value}")
|
||
entry.append(f"- Fingerprint: {result.fingerprint}")
|
||
entry.append("")
|
||
|
||
with open(log_path, "a") as f:
|
||
f.write("\n".join(entry))
|
||
|
||
|
||
def main():
|
||
"""CLI entry point."""
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description="Prompt Guard - Injection Detection")
|
||
parser.add_argument("message", nargs="?", help="Message to analyze")
|
||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||
parser.add_argument("--context", type=str, help="Context as JSON string")
|
||
parser.add_argument("--config", type=str, help="Path to config YAML")
|
||
parser.add_argument(
|
||
"--sensitivity",
|
||
choices=["low", "medium", "high", "paranoid"],
|
||
default="medium",
|
||
help="Detection sensitivity",
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
if not args.message:
|
||
# Read from stdin
|
||
args.message = sys.stdin.read().strip()
|
||
|
||
if not args.message:
|
||
parser.print_help()
|
||
sys.exit(1)
|
||
|
||
config = {"sensitivity": args.sensitivity}
|
||
if args.config:
|
||
try:
|
||
import yaml
|
||
except ImportError:
|
||
print(
|
||
"Error: PyYAML required for config files. Install with: pip install pyyaml",
|
||
file=sys.stderr,
|
||
)
|
||
sys.exit(1)
|
||
with open(args.config) as f:
|
||
file_config = yaml.safe_load(f) or {}
|
||
file_config = file_config.get("prompt_guard", file_config)
|
||
config.update(file_config)
|
||
|
||
# Parse context
|
||
context = {}
|
||
if args.context:
|
||
context = json.loads(args.context)
|
||
|
||
# Analyze
|
||
guard = PromptGuard(config)
|
||
result = guard.analyze(args.message, context)
|
||
|
||
if args.json:
|
||
print(json.dumps(result.to_dict(), indent=2, ensure_ascii=False))
|
||
else:
|
||
emoji = {
|
||
"SAFE": "✅",
|
||
"LOW": "📝",
|
||
"MEDIUM": "⚠️",
|
||
"HIGH": "🔴",
|
||
"CRITICAL": "🚨",
|
||
}
|
||
print(f"{emoji.get(result.severity.name, '❓')} {result.severity.name}")
|
||
print(f"Action: {result.action.value}")
|
||
if result.reasons:
|
||
print(f"Reasons: {', '.join(result.reasons)}")
|
||
if result.patterns_matched:
|
||
print(f"Patterns: {len(result.patterns_matched)} matched")
|
||
if result.normalized_text:
|
||
print(f"⚠️ Homoglyphs detected, normalized text differs")
|
||
if result.base64_findings:
|
||
print(f"⚠️ Suspicious base64: {len(result.base64_findings)} found")
|
||
if result.recommendations:
|
||
print(f"💡 {'; '.join(result.recommendations)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|