diff --git a/README.md b/README.md index 4255c5f..bbd1c2d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Python module for structured prompt orchestration with: - configurable summary LLM with provider selection - TTL cache backends - optional RAG providers -- safety checks (injection + contradiction heuristics) +- safety checks (config-driven grouped threats, weighted groups, bilingual patterns, contradiction pairs) - prompt efficiency analyzer - token counting with tiktoken - centralized mutable config (Pydantic) @@ -115,6 +115,29 @@ Use it as a panel/query blueprint in SigNoz to create a dashboard for prompt bui - `ModuleConfig`: full module config in one object - `ConfigStore`: mutable config holder (`get`, `set_config`, `as_dict`) +## Safety Engine + +The safety layer is configured from [prompt_orchestrator/safety/threats.json](prompt_orchestrator/safety/threats.json). The catalog is grouped by threat family, and each family has its own weight so the final severity is still computed by the maximum matched threat score. + +What changed: + +- threat families are defined in `threats.json` and loaded at runtime +- regular lexical rules live under `patterns` +- contradiction rules live under `contradictions` and are matched as pairs +- each family can include English and Russian analogs for the same threat family +- duplicate patterns were removed from the catalog +- each matched rule keeps its threat code in the report + +`SafetyReport` now includes: + +- `issues`: flat list of matched safety issues +- `threat_groups`: grouped report by threat family +- `severity`: overall severity (`none`, `low`, `medium`, `high`) +- `threat_score`: weighted maximum score used for the final severity +- `sanitized_prompt`: optional rewritten prompt when auto rewrite is enabled + +Each grouped report includes the threat family name, the number of matches, the matched codes, and the family weight. Use `result.safety.grouped_summary` or `result.safety.model_dump()` to inspect the grouped output. + ### OrchestratorSettings.debug_mode By default, section headers (`=== STATIC PART (CACHE-FRIENDLY) ===`, etc.) are **excluded** from the final prompt sent to LLMs to save tokens. diff --git a/prompt_orchestrator/safety/__init__.py b/prompt_orchestrator/safety/__init__.py index 5d843a5..e91b170 100644 --- a/prompt_orchestrator/safety/__init__.py +++ b/prompt_orchestrator/safety/__init__.py @@ -1,4 +1,4 @@ from .engine import PromptSafetyEngine -from .report import SafetyIssue, SafetyReport +from .report import SafetyIssue, SafetyReport, SafetyThreatGroupReport -__all__ = ["PromptSafetyEngine", "SafetyIssue", "SafetyReport"] +__all__ = ["PromptSafetyEngine", "SafetyIssue", "SafetyReport", "SafetyThreatGroupReport"] diff --git a/prompt_orchestrator/safety/engine.py b/prompt_orchestrator/safety/engine.py index ba93e9c..822316d 100644 --- a/prompt_orchestrator/safety/engine.py +++ b/prompt_orchestrator/safety/engine.py @@ -1,49 +1,233 @@ from __future__ import annotations +import json import re +import unicodedata +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path +from typing import Literal -from .report import SafetyIssue, SafetyReport +from .report import SafetyIssue, SafetyReport, SafetyThreatGroupReport + + +Severity = Literal["none", "low", "medium", "high"] + + +@dataclass(frozen=True, slots=True) +class ThreatRule: + code: str + pattern: str | None = None + contradiction: tuple[str, str] | None = None + compiled_pattern: re.Pattern[str] | None = None + + +@dataclass(frozen=True, slots=True) +class ThreatGroup: + key: str + description: str + risk_level: Severity + weight: float + rules: tuple[ThreatRule, ...] + + +def _severity_from_score(score: float) -> Severity: + if score >= 0.85: + return "high" + if score >= 0.5: + return "medium" + if score > 0: + return "low" + return "none" + + +def _weight_from_risk_level(risk_level: str) -> float: + mapping = { + "high": 1.0, + "medium": 0.65, + "low": 0.35, + "none": 0.0, + } + return mapping.get(risk_level, 0.0) + + +@lru_cache(maxsize=1) +def _load_threat_groups() -> tuple[ThreatGroup, ...]: + threats_path = Path(__file__).with_name("threats.json") + raw_data = json.loads(threats_path.read_text(encoding="utf-8")) + + seen_rules: set[tuple[str, str]] = set() + groups: list[ThreatGroup] = [] + + for key, payload in raw_data.items(): + description = str(payload.get("description", "")) + risk_level = str(payload.get("risk_level", "none")) + weight = float(payload.get("weight", _weight_from_risk_level(risk_level))) + rules: list[ThreatRule] = [] + + for entry in payload.get("patterns", payload.get("data", [])): + if "pattern" in entry: + pattern = str(entry["pattern"]) + dedupe_key = ("pattern", pattern) + if dedupe_key in seen_rules: + continue + seen_rules.add(dedupe_key) + rules.append( + ThreatRule( + code=str(entry["code"]), + pattern=pattern, + compiled_pattern=re.compile(pattern, re.IGNORECASE), + ) + ) + for entry in payload.get("contradictions", []): + contradiction_values = tuple(str(value) for value in entry["contradiction"]) + dedupe_key = ("contradiction", "||".join(contradiction_values)) + if dedupe_key in seen_rules: + continue + seen_rules.add(dedupe_key) + if len(contradiction_values) != 2: + continue + rules.append( + ThreatRule( + code=str(entry["code"]), + contradiction=(contradiction_values[0], contradiction_values[1]), + ) + ) + + if not rules and "data" in payload: + for entry in payload.get("data", []): + if "pattern" in entry: + pattern = str(entry["pattern"]) + dedupe_key = ("pattern", pattern) + if dedupe_key in seen_rules: + continue + seen_rules.add(dedupe_key) + rules.append( + ThreatRule( + code=str(entry["code"]), + pattern=pattern, + compiled_pattern=re.compile(pattern, re.IGNORECASE), + ) + ) + elif "contradiction" in entry: + contradiction_values = tuple(str(value) for value in entry["contradiction"]) + dedupe_key = ("contradiction", "||".join(contradiction_values)) + if dedupe_key in seen_rules: + continue + seen_rules.add(dedupe_key) + if len(contradiction_values) != 2: + continue + rules.append( + ThreatRule( + code=str(entry["code"]), + contradiction=(contradiction_values[0], contradiction_values[1]), + ) + ) + + groups.append( + ThreatGroup( + key=key, + description=description, + risk_level=risk_level if risk_level in {"none", "low", "medium", "high"} else "none", + weight=weight, + rules=tuple(rules), + ) + ) + + return tuple(groups) class PromptSafetyEngine: - INJECTION_PATTERNS = [ - re.compile(r"ignore\s+previous\s+instructions", re.IGNORECASE), - re.compile(r"(reveal|print|show)\s+(the\s+)?system\s+prompt", re.IGNORECASE), - re.compile(r"(reveal|print|show)\s+(the\s+)?developer\s+message", re.IGNORECASE), - re.compile(r"reveal\s+hidden", re.IGNORECASE), - ] + def __init__(self) -> None: + self._threat_groups = _load_threat_groups() + + def _too_many_new_lines(self, prompt: str) -> bool: + return prompt.count("\n") > 300 + + def _prompt_too_long(self, prompt: str) -> bool: + return len(prompt) > 15000 + + def analize(self, prompt: str) -> SafetyReport: + return self.analyze(prompt) def analyze(self, prompt: str) -> SafetyReport: + prompt = unicodedata.normalize("NFKC", prompt) + prompt_casefold = prompt.casefold() + issues: list[SafetyIssue] = [] + threat_groups: list[SafetyThreatGroupReport] = [] + highest_score = 0.0 - for pattern in self.INJECTION_PATTERNS: - if pattern.search(prompt): - issues.append( - SafetyIssue( - code="prompt_injection", - message=f"Potential injection marker detected: {pattern.pattern}", - severity="high", - ) + if self._prompt_too_long(prompt): + score = 0.5 + issues.append( + SafetyIssue( + code="prompt_too_long", + message=f"Prompt length {len(prompt)} exceeds safe threshold.", + severity="medium", + weight=score, ) + ) + highest_score = max(highest_score, score) - if "always answer in json" in prompt.lower() and "never use json" in prompt.lower(): + if self._too_many_new_lines(prompt): + score = 0.5 issues.append( SafetyIssue( - code="contradiction", - message="Conflicting output constraints found.", + code="too_many_newlines", + message=f"Prompt contains {prompt.count(chr(10))} newlines, which may indicate an attempt to obfuscate content.", severity="medium", + weight=score, ) ) + highest_score = max(highest_score, score) + + for group in self._threat_groups: + findings: list[SafetyIssue] = [] + for rule in group.rules: + matched = False + if rule.compiled_pattern is not None and rule.compiled_pattern.search(prompt): + matched = True + elif rule.contradiction is not None: + left = unicodedata.normalize("NFKC", rule.contradiction[0]).casefold() + right = unicodedata.normalize("NFKC", rule.contradiction[1]).casefold() + matched = left in prompt_casefold and right in prompt_casefold + + if not matched: + continue + + severity = _severity_from_score(group.weight) + issue = SafetyIssue( + code=rule.code, + message=f"[{group.key}] matched threat code {rule.code}", + severity=severity, + group=group.key, + pattern=rule.pattern if rule.pattern is not None else " / ".join(rule.contradiction or ()), + weight=group.weight, + ) + issues.append(issue) + findings.append(issue) + highest_score = max(highest_score, group.weight) + + if findings: + threat_groups.append( + SafetyThreatGroupReport( + name=group.key, + description=group.description, + risk_level=group.risk_level, + weight=group.weight, + issues=findings, + ) + ) - severity = "none" - if any(i.severity == "high" for i in issues): - severity = "high" - elif any(i.severity == "medium" for i in issues): - severity = "medium" - elif issues: - severity = "low" + severity = _severity_from_score(highest_score) - return SafetyReport(issues=issues, severity=severity) + return SafetyReport( + issues=issues, + threat_groups=threat_groups, + severity=severity, + threat_score=highest_score, + ) def sanitize(self, prompt: str) -> str: sanitized = prompt @@ -53,6 +237,18 @@ def sanitize(self, prompt: str) -> str: sanitized, flags=re.IGNORECASE, ) + sanitized = re.sub( + r"ignore\s+all\s+rules", + "[REMOVED_INJECTION_PATTERN]", + sanitized, + flags=re.IGNORECASE, + ) + sanitized = re.sub( + r"from\s+now\s+on", + "[REMOVED_INJECTION_PATTERN]", + sanitized, + flags=re.IGNORECASE, + ) sanitized = re.sub( r"reveal\s+hidden", "[REMOVED_SENSITIVE_REQUEST]", diff --git a/prompt_orchestrator/safety/report.py b/prompt_orchestrator/safety/report.py index a2b7252..9fe454d 100644 --- a/prompt_orchestrator/safety/report.py +++ b/prompt_orchestrator/safety/report.py @@ -5,17 +5,52 @@ from pydantic import BaseModel, Field +Severity = Literal["none", "low", "medium", "high"] + + class SafetyIssue(BaseModel): code: str message: str - severity: Literal["low", "medium", "high"] = "low" + severity: Severity = "none" + group: str | None = None + pattern: str | None = None + weight: float | None = None + + +class SafetyThreatGroupReport(BaseModel): + name: str + description: str + risk_level: Severity = "none" + weight: float = 0.0 + issues: list[SafetyIssue] = Field(default_factory=list) + + @property + def count(self) -> int: + return len(self.issues) + + @property + def codes(self) -> list[str]: + return [issue.code for issue in self.issues] class SafetyReport(BaseModel): issues: list[SafetyIssue] = Field(default_factory=list) - severity: Literal["none", "low", "medium", "high"] = "none" + threat_groups: list[SafetyThreatGroupReport] = Field(default_factory=list) + severity: Severity = "none" + threat_score: float = 0.0 sanitized_prompt: str | None = None @property def is_safe(self) -> bool: return self.severity in {"none", "low"} + + @property + def grouped_summary(self) -> str: + if not self.threat_groups: + return "" + + lines: list[str] = [] + for index, group in enumerate(self.threat_groups, start=1): + codes = ", ".join(group.codes) if group.codes else "None" + lines.append(f"{index}. {group.name}: {group.count} threat(s), codes: {codes}") + return "\n".join(lines) diff --git a/prompt_orchestrator/safety/threats.json b/prompt_orchestrator/safety/threats.json new file mode 100644 index 0000000..76b7016 --- /dev/null +++ b/prompt_orchestrator/safety/threats.json @@ -0,0 +1,283 @@ +{ + "prompt_injection": { + "description": "The user tries to inject malicious code into the prompt to manipulate the model's behavior.", + "risk_level": "high", + "weight": 1.0, + "data": [ + {"pattern": "ignore\\s+previous\\s+instructions", "code": "PI1"}, + {"pattern": "игнорируй\\s+(все\\s+)?предыдущие\\s+инструкции", "code": "PI2"}, + {"pattern": "bypass\\s+filters", "code": "PI2"}, + {"pattern": "обойди\\s+фильтры", "code": "PI3"}, + {"pattern": "execute\\s+malicious\\s+code", "code": "PI3"}, + {"pattern": "выполни\\s+вредоносный\\s+код", "code": "PI4"}, + {"pattern": "reveal\\s+hidden", "code": "PI4"}, + {"pattern": "не\\s+обращай\\s+внимания\\s+на\\s+предыдущие\\s+инструкции", "code": "PI6"}, + {"pattern": "забудь\\s+(все\\s+)?предыдущие\\s+инструкции", "code": "PI7"}, + {"pattern": "забудь\\s+(все\\s+)?предыдущие\\s+сообщения", "code": "PI8"}, + {"pattern": "забудь\\s+(все\\s+)?предыдущие\\s+контексты?", "code": "PI9"}, + {"pattern": "забудь\\s+(все\\s+)?предыдущие\\s+данные", "code": "PI10"}, + {"pattern": "отмени\\s+все\\s+ограничения", "code": "PI11"}, + {"pattern": "отключи\\s+фильтры", "code": "PI12"}, + {"pattern": "отключи\\s+модерацию", "code": "PI13"}, + {"pattern": "перестань\\s+следовать\\s+правилам", "code": "PI14"}, + {"pattern": "сними\\s+ограничения", "code": "PI15"}, + {"pattern": "покажи\\s+скрытые\\s+инструкции", "code": "PI16"} + ] + }, + "instruction_override": { + "description": "The user attempts to override or manipulate the model's instructions to achieve unintended behavior.", + "risk_level": "high", + "weight": 0.95, + "data": [ + {"pattern": "ignore\\s+(all\\s+)?(rules|guidelines|constraints)", "code": "IO1"}, + {"pattern": "игнорируй\\s+(все\\s+)?(правила|инструкции|ограничения)", "code": "IO2"}, + {"pattern": "(bypass|circumvent)\\s+(the\\s+)?(safety\\s+)?(measures|protections)", "code": "IO2"}, + {"pattern": "(обойди|обходи|игнорируй)\\s+(механизмы\\s+)?(безопасности|защиты)", "code": "IO3"}, + {"pattern": "(override|change|modify)\\s+(the\\s+)?(default\\s+)?(behavior|response)", "code": "IO3"}, + {"pattern": "(ignore|disregard)\\s+(the\\s+)?(model's)?(limitations|restrictions)", "code": "IO4"}, + {"pattern": "(force|compel)\\s+(the\\s+)?model\\s+to\\s+(perform|execute)\\s+(an\\s+)?action", "code": "IO5"}, + {"pattern": "forget\\s+(everything|all\\s+previous)", "code": "IO6"}, + {"pattern": "new\\s+instructions?:", "code": "IO7"}, + {"pattern": "from\\s+now\\s+on", "code": "IO8"}, + {"pattern": "you\\s+must\\s+obey", "code": "IO9"}, + {"pattern": "с\\s+этого\\s+момента", "code": "IO10"}, + {"pattern": "ты\\s+должен\\s+подчиняться", "code": "IO11"} + ] + }, + "prompt_extraction": { + "description": "The user tries to extract sensitive information from the model's responses or prompts.", + "risk_level": "medium", + "weight": 0.75, + "data": [ + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(system\\s+)?(prompt|instructions)", "code": "PE1"}, + {"pattern": "(покажи|выведи|раскрой)\\s+(системный\\s+)?(промпт|инструкции)", "code": "PE2"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(developer\\s+)?(message|notes)", "code": "PE2"}, + {"pattern": "(покажи|выведи|раскрой)\\s+(инструкции\\s+разработчика|заметки\\s+разработчика)", "code": "PE3"}, + {"pattern": "(extract|steal)\\s+(the\\s+)?(model's)?(knowledge|data)", "code": "PE3"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(training\\s+)?(data|examples)", "code": "PE4"}, + {"pattern": "repeat\\s+your\\s+instructions", "code": "PE5"}, + {"pattern": "dump\\s+prompt", "code": "PE6"}, + {"pattern": "verbatim\\s+instructions", "code": "PE7"}, + {"pattern": "покажи\\s+системный\\s+промпт", "code": "PE8"}, + {"pattern": "выведи\\s+системный\\s+промпт", "code": "PE9"}, + {"pattern": "раскрой\\s+системные\\s+инструкции", "code": "PE10"}, + {"pattern": "покажи\\s+скрытые\\s+инструкции", "code": "PE11"}, + {"pattern": "покажи\\s+инструкции\\s+разработчика", "code": "PE12"}, + {"pattern": "процитируй\\s+свои\\s+инструкции", "code": "PE13"}, + {"pattern": "выведи\\s+скрытые\\s+данные", "code": "PE14"} + ] + }, + "role_manipulation": { + "description": "The user attempts to manipulate the model's role or identity to achieve unintended behavior.", + "risk_level": "medium", + "weight": 0.55, + "data": [ + {"pattern": "(pretend|act|behave)\\s+as\\s+(a\\s+)?(different\\s+)?(entity|personality)", "code": "RM1"}, + {"pattern": "(притворись|веди\\s+себя|действуй)\\s+как\\s+(другая\\s+)?(сущность|личность)", "code": "RM2"}, + {"pattern": "(assume|take)\\s+(the\\s+)?(role|identity)\\s+of\\s+(a\\s+)?(different\\s+)?(entity|personality)", "code": "RM2"}, + {"pattern": "(прими|возьми)\\s+на\\s+себя\\s+(роль|личность)", "code": "RM3"}, + {"pattern": "(switch|change)\\s+(the\\s+)?(role|identity)", "code": "RM3"}, + {"pattern": "(impersonate|mimic)\\s+(a\\s+)?(different\\s+)?(entity|personality)", "code": "RM4"}, + {"pattern": "(adopt|take on)\\s+(a\\s+)?(different\\s+)?(persona|character)", "code": "RM5"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?current\\s+(role|identity)", "code": "RM6"}, + {"pattern": "act\\s+as\\s+an?\\s+unrestricted", "code": "RM7"}, + {"pattern": "act\\s+as\\s+root", "code": "RM8"}, + {"pattern": "pretend\\s+to\\s+be", "code": "RM9"}, + {"pattern": "you\\s+are\\s+now", "code": "RM10"}, + {"pattern": "представь\\s+что\\s+ты", "code": "RM11"}, + {"pattern": "теперь\\s+ты", "code": "RM12"}, + {"pattern": "притворись\\s+что\\s+ты", "code": "RM13"}, + {"pattern": "веди\\s+себя\\s+как", "code": "RM14"}, + {"pattern": "веди\\s+себя\\s+как\\s+неограниченный", "code": "RM15"}, + {"pattern": "веди\\s+себя\\s+как\\s+root", "code": "RM16"}, + {"pattern": "действуй\\s+как", "code": "RM17"}, + {"pattern": "выступай\\s+в\\s+роли", "code": "RM18"}, + {"pattern": "поведи\\s+себя\\s+как", "code": "RM19"} + ] + }, + "jailbreak_attempt": { + "description": "The user tries to jailbreak the model to bypass safety measures and restrictions.", + "risk_level": "high", + "weight": 1.0, + "data": [ + {"pattern": "(jailbreak|bypass)\\s+(the\\s+)?(safety\\s+)?(measures|protections)", "code": "JA1"}, + {"pattern": "(обойди|обойди\\s+защиту|обходи)\\s+(механизмы\\s+)?(безопасности|защиты)", "code": "JA2"}, + {"pattern": "(disable|remove)\\s+(the\\s+)?(safety\\s+)?(features|mechanisms|restrictions)", "code": "JA2"}, + {"pattern": "(отключи|удали)\\s+(функции\\s+)?(безопасности|ограничения)", "code": "JA3"}, + {"pattern": "(circumvent|evade)\\s+(the\\s+)?(model's)?(restrictions|limitations)", "code": "JA3"}, + {"pattern": "developer\\s+mode", "code": "JA4"}, + {"pattern": "dan\\s+mode", "code": "JA5"}, + {"pattern": "do\\s+anything\\s+now", "code": "JA6"}, + {"pattern": "режим\\s+разработчика", "code": "JA7"}, + {"pattern": "режим\\s+без\\s+ограничений", "code": "JA8"}, + {"pattern": "сними\\s+ограничения", "code": "JA9"}, + {"pattern": "обойди\\s+фильтры", "code": "JA10"}, + {"pattern": "обойди\\s+политику\\s+безопасности", "code": "JA11"}, + {"pattern": "сними\\s+все\\s+ограничения", "code": "JA12"} + ] + }, + "context_manipulation": { + "description": "The user attempts to manipulate the context or conversation history to achieve unintended behavior.", + "risk_level": "medium", + "weight": 0.6, + "data": [ + {"pattern": "(ignore|disregard)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM1"}, + {"pattern": "(игнорируй|не\\s+обращай\\s+внимания\\s+на)\\s+(историю|контекст)(\\s+разговора)?", "code": "CM2"}, + {"pattern": "(forget|erase)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM2"}, + {"pattern": "(забудь|удали|стереть)\\s+(историю|контекст)(\\s+разговора)?", "code": "CM3"}, + {"pattern": "(manipulate|alter)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM3"}, + {"pattern": "(add|insert)\\s+(false\\s+)?(information|context)", "code": "CM4"}, + {"pattern": "(remove|delete)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM5"}, + {"pattern": "(change|modify)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM6"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(conversation\\s+)?(history|context)", "code": "CM7"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?previous", "code": "CM8"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(last\\s+)?(response|message)", "code": "CM9"}, + {"pattern": "(reveal|print|show)\\s+(the\\s+)?(previous)?(response|message)", "code": "CM10"}, + {"pattern": "everything\\s+above\\s+is\\s+irrelevant", "code": "CM11"}, + {"pattern": "disregard\\s+previous", "code": "CM12"}, + {"pattern": "всё\\s+выше\\s+неважно", "code": "CM13"} + ] + }, + "secrets_leak": { + "description": "The user tries to extract or reveal sensitive information from the model's responses or prompts.", + "risk_level": "high", + "weight": 0.9, + "data": [ + {"pattern": "api[_-]?key", "code": "SL1"}, + {"pattern": "api[_-]?ключ", "code": "SL2"}, + {"pattern": "access[_-]?token", "code": "SL2"}, + {"pattern": "токен\\s+доступа", "code": "SL3"}, + {"pattern": "secret[_-]?key", "code": "SL3"}, + {"pattern": "секретный\\s+ключ", "code": "SL4"}, + {"pattern": "private[_-]?key", "code": "SL4"}, + {"pattern": "приватный\\s+ключ", "code": "SL5"}, + {"pattern": "password", "code": "SL5"}, + {"pattern": "aws[_-]?secret", "code": "SL6"}, + {"pattern": "покажи\\s+пароль", "code": "SL7"}, + {"pattern": "покажи\\s+ключ\\s+доступа", "code": "SL8"}, + {"pattern": "покажи\\s+секрет", "code": "SL9"}, + {"pattern": "покажи\\s+ключ", "code": "SL10"}, + {"pattern": "покажи\\s+токен", "code": "SL11"}, + {"pattern": "покажи\\s+конфиденциальную\\s+информацию", "code": "SL12"}, + {"pattern": "покажи\\s+секретные\\s+данные", "code": "SL13"}, + {"pattern": "покажи\\s+личные\\s+данные", "code": "SL14"}, + {"pattern": "раскрой\\s+токен", "code": "SL15"}, + {"pattern": "выведи\\s+секрет", "code": "SL16"} + ] + }, + "malicious_code_injection": { + "description": "The user tries to inject malicious code into the prompt to manipulate the model's behavior.", + "risk_level": "high", + "weight": 0.85, + "data": [ + {"pattern": "