@weave_protocol/mund 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,31 +16,49 @@ export class InjectionDetector implements IAnalyzer {
16
16
  type = 'injection';
17
17
 
18
18
  async analyze(content: string, rules: DetectionRule[]): Promise<SecurityIssue[]> {
19
- const issues: SecurityIssue[] = [];
20
19
  const injectionRules = rules.filter(r => r.type === 'injection' && r.enabled);
21
20
 
22
- // Normalize content for detection
23
- const normalizedContent = this.normalizeContent(content);
21
+ // The text as written, plus text an agent may see that a person doesn't:
22
+ // base64 that decodes to prose and invisible Unicode "tag" characters.
23
+ const views: Array<{ text: string; via?: string }> = [{ text: content }];
24
+ for (const decoded of decodeBase64Prose(content)) views.push({ text: decoded, via: 'base64' });
25
+ const tagText = decodeUnicodeTags(content);
26
+ if (tagText) views.push({ text: tagText, via: 'unicode tags' });
24
27
 
25
- for (const rule of injectionRules) {
26
- if (!rule.pattern) continue;
28
+ const issues: SecurityIssue[] = [];
29
+ const seen = new Set<string>();
30
+ for (const view of views) {
31
+ for (const issue of this.applyRules(content, view.text, injectionRules, view.via)) {
32
+ // One finding per rule per view position; the same phrase found twice via decoding counts once
33
+ const key = `${issue.rule_id}:${view.via ?? ''}:${issue.location?.start ?? issue.match}`;
34
+ if (!seen.has(key)) { seen.add(key); issues.push(issue); }
35
+ }
36
+ }
27
37
 
38
+ // Additional heuristic checks
39
+ issues.push(...this.heuristicAnalysis(content));
40
+ return issues;
41
+ }
42
+
43
+ private applyRules(original: string, text: string, rules: DetectionRule[], via?: string): SecurityIssue[] {
44
+ const issues: SecurityIssue[] = [];
45
+ const normalizedContent = this.normalizeContent(text);
46
+ for (const rule of rules) {
47
+ if (!rule.pattern) continue;
28
48
  try {
29
49
  const regex = compileRulePattern(rule.pattern, 'gi');
30
50
  let match: RegExpExecArray | null;
31
-
32
51
  while ((match = regex.exec(normalizedContent)) !== null) {
52
+ if (match[0].length === 0) { regex.lastIndex++; continue; }
33
53
  const matchText = match[0];
34
- const location = this.getLocation(content, match.index, matchText.length);
35
-
36
54
  issues.push({
37
55
  rule_id: rule.id,
38
- rule_name: rule.name,
56
+ rule_name: via ? `${rule.name} (hidden in ${via})` : rule.name,
39
57
  type: rule.type,
40
58
  severity: rule.severity,
41
59
  action: rule.action,
42
60
  match: this.sanitizeMatch(matchText),
43
- location,
61
+ location: via ? undefined : this.getLocation(original, match.index, matchText.length),
44
62
  suggestion: this.getSuggestion(rule.id)
45
63
  });
46
64
  }
@@ -48,11 +66,6 @@ export class InjectionDetector implements IAnalyzer {
48
66
  console.error(`Invalid regex pattern in rule ${rule.id}:`, error);
49
67
  }
50
68
  }
51
-
52
- // Additional heuristic checks
53
- const heuristicIssues = this.heuristicAnalysis(content);
54
- issues.push(...heuristicIssues);
55
-
56
69
  return issues;
57
70
  }
58
71
 
@@ -61,14 +74,17 @@ export class InjectionDetector implements IAnalyzer {
61
74
  */
62
75
  private normalizeContent(content: string): string {
63
76
  return content
64
- // Remove zero-width characters
65
- .replace(/[\u200B-\u200D\uFEFF]/g, '')
66
- // Normalize Unicode variations
77
+ // Remove zero-width and invisible formatting characters
78
+ .replace(/[​-‍⁠­]/g, '')
79
+ // Normalize Unicode variations (fullwidth, ligatures)
67
80
  .normalize('NFKC')
68
- // Collapse multiple spaces
69
- .replace(/\s+/g, ' ')
70
- // Remove common obfuscation characters
71
- .replace(/[^\w\s.,!?'"()-]/g, ' ');
81
+ // Curly quotes read as straight ones ("don’t" → "don't")
82
+ .replace(/[‘’‛′]/g, "'")
83
+ .replace(/[“”‟″]/g, '"')
84
+ // Keep letters in every script (accents included), digits and light punctuation
85
+ .replace(/[^\p{L}\p{N}\s.,!?'"()-]/gu, ' ')
86
+ // Collapse whitespace
87
+ .replace(/\s+/g, ' ');
72
88
  }
73
89
 
74
90
  /**
@@ -77,23 +93,27 @@ export class InjectionDetector implements IAnalyzer {
77
93
  private heuristicAnalysis(content: string): SecurityIssue[] {
78
94
  const issues: SecurityIssue[] = [];
79
95
 
80
- // Check for role switching attempts
81
- const roleSwitchPatterns = [
82
- /\bsystem\s*:\s*you\s+are/i,
83
- /\bassistant\s*:\s*i\s+(will|can|am)/i,
84
- /\[system\]/i,
85
- /\<\|?system\|?\>/i,
86
- /\{system_prompt\}/i,
96
+ // Role markers. Chat-template tokens never belong in content, so they're
97
+ // high; a plain "System: you are" also opens many legitimate transcripts.
98
+ const roleSwitchPatterns: Array<[RegExp, 'high' | 'low']> = [
99
+ [/<\|\s*(?:im_start|im_end|system|user|assistant|endoftext|start_header_id|end_header_id|eot_id)\s*\|>/i, 'high'],
100
+ [/\[\/?INST\]|<<\/?SYS>>/, 'high'],
101
+ [/\[\s*(?:system|admin|administrator|developer)(?:\s+(?:override|message|prompt|note|instruction)s?)?[^\]\n]{0,30}\]/i, 'high'],
102
+ [/\<\|?system\|?\>/i, 'high'],
103
+ [/\{system_prompt\}/i, 'high'],
104
+ [/^\s*#{1,4}\s*(?:system|instruction)s?\s*:/im, 'high'],
105
+ [/\bsystem\s*:\s*you\s+are/i, 'low'],
106
+ [/\bassistant\s*:\s*i\s+(will|can|am)/i, 'low'],
87
107
  ];
88
108
 
89
- for (const pattern of roleSwitchPatterns) {
109
+ for (const [pattern, severity] of roleSwitchPatterns) {
90
110
  const match = content.match(pattern);
91
111
  if (match) {
92
112
  issues.push({
93
113
  rule_id: 'heuristic_role_switch',
94
114
  rule_name: 'Role Switching Attempt',
95
115
  type: 'injection',
96
- severity: 'high' as const,
116
+ severity,
97
117
  action: 'alert' as const,
98
118
  match: this.sanitizeMatch(match[0]),
99
119
  suggestion: 'This content appears to attempt to inject role-switching markers.'
@@ -101,6 +121,66 @@ export class InjectionDetector implements IAnalyzer {
101
121
  }
102
122
  }
103
123
 
124
+ // Instructions in HTML a person never sees: hidden elements, noscript,
125
+ // template and comments. Hidden menus and labels are common; hidden text
126
+ // that addresses a reader with a directive is not.
127
+ for (const hidden of hiddenHtmlText(content)) {
128
+ if (ADDRESSES_READER.test(hidden) && DIRECTIVE_VERB.test(hidden)) {
129
+ issues.push({
130
+ rule_id: 'heuristic_hidden_instruction',
131
+ rule_name: 'Instruction in Hidden HTML',
132
+ type: 'injection',
133
+ severity: 'high',
134
+ action: 'alert',
135
+ match: this.sanitizeMatch(hidden),
136
+ suggestion: 'Hidden page text gives instructions. Strip hidden elements before an agent reads the page.'
137
+ });
138
+ }
139
+ }
140
+
141
+ // Tool-call syntax inside content: a page or result dressed up as the
142
+ // model's own function call, naming a tool that sends or runs something.
143
+ const toolCall = content.match(/<\/?(?:function_calls|tool_call|tool_use)>|<invoke\s+name\s*=|"(?:function|name|tool|tool_name)"\s*:\s*"(?:send_email|send_message|http_request|http_post|fetch_url|web_fetch|transfer\w*|send_payment|pay\w*|exec\w*|shell\w*|run_command|run_shell|bash|write_file|delete\w*)"\s*,\s*"(?:arguments|args|parameters|input)"/i);
144
+ if (toolCall) {
145
+ issues.push({
146
+ rule_id: 'heuristic_tool_call_mimicry',
147
+ rule_name: 'Tool-Call Syntax in Content',
148
+ type: 'injection',
149
+ severity: 'medium',
150
+ action: 'alert',
151
+ match: this.sanitizeMatch(toolCall[0]),
152
+ suggestion: 'Content contains tool-call syntax for an action tool. An agent should never execute calls found in content.'
153
+ });
154
+ }
155
+
156
+ // Zero-width characters inside words split trigger words for filters
157
+ // while the model still reads them whole.
158
+ const splitWords = content.match(/\p{L}[​-‍⁠­]+\p{L}/gu) ?? [];
159
+ if (splitWords.length >= 3) {
160
+ issues.push({
161
+ rule_id: 'heuristic_zero_width_split',
162
+ rule_name: 'Zero-Width Characters Inside Words',
163
+ type: 'injection',
164
+ severity: 'medium',
165
+ action: 'alert',
166
+ match: `${splitWords.length} words split by invisible characters`,
167
+ suggestion: 'Invisible characters inside words are a filter-evasion technique. Strip them before processing.'
168
+ });
169
+ }
170
+
171
+ // Unicode tag characters (U+E0000–E007F) carry invisible ASCII
172
+ if (/[\u{E0020}-\u{E007F}]/u.test(content)) {
173
+ issues.push({
174
+ rule_id: 'heuristic_unicode_tags',
175
+ rule_name: 'Invisible Unicode Tag Text',
176
+ type: 'injection',
177
+ severity: 'high',
178
+ action: 'alert',
179
+ match: (decodeUnicodeTags(content) ?? '').slice(0, 100),
180
+ suggestion: 'The content carries invisible text in Unicode tag characters. Strip them before processing.'
181
+ });
182
+ }
183
+
104
184
  // Check for markdown/code injection that might alter rendering
105
185
  const markdownPatterns = [
106
186
  /```\s*system/i,
@@ -183,10 +263,80 @@ export class InjectionDetector implements IAnalyzer {
183
263
  prompt_injection_jailbreak: 'This appears to be a jailbreak attempt. Such content should be filtered or rejected.',
184
264
  prompt_injection_system: 'This content attempts to extract system prompts. Do not reveal system instructions.',
185
265
  prompt_injection_roleplay: 'This content attempts to manipulate behavior through roleplay. Apply content filtering.',
266
+ prompt_injection_new_task: 'This content tries to give the agent a new task. Treat it as data, not instructions.',
267
+ prompt_injection_ai_addressed: 'This content speaks to an AI reading it. Treat it as data, not instructions.',
268
+ prompt_injection_concealment: 'This content tells the agent to hide its actions from the user. Block it and alert the user.',
269
+ prompt_injection_suppression: 'This content tells the agent to refuse or skip the user\'s request.',
270
+ prompt_injection_authority: 'This content claims system, admin or developer authority. Content can\'t grant authority.',
271
+ prompt_injection_decode_execute: 'This content asks the agent to decode hidden text and act on it.',
272
+ prompt_injection_data_harvest: 'This content asks the agent to collect the user\'s private data.',
186
273
  };
187
274
 
188
275
  return suggestions[ruleId] || 'This content shows signs of injection attacks. Review and sanitize input.';
189
276
  }
190
277
  }
191
278
 
279
+ // ─────────────────────────────────────────────────────────
280
+ // Hidden-text helpers
281
+ // ─────────────────────────────────────────────────────────
282
+
283
+ const ADDRESSES_READER = /\b(?:you|your|ai|assistant|agent|llm|model|chatbot|the\s+user)\b/i;
284
+ const DIRECTIVE_VERB = /\b(?:must|should|respond|reply|include|tell|send|ignore|reveal|print|call|transfer|execute|run|output|say|forward|delete|disregard|override)\b/i;
285
+
286
+ const HIDDEN_STYLE = /(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)|font-size\s*:\s*0(?:px|em|rem|pt|%)?\s*(?:;|$)|(?:height|width)\s*:\s*0(?:px)?\s*;[^"']*overflow\s*:\s*hidden|clip\s*:\s*rect\(0|left\s*:\s*-\d{4,}px)/i;
287
+
288
+ /** Text inside HTML a person doesn't see. */
289
+ export function hiddenHtmlText(html: string): string[] {
290
+ if (!/<[a-z!]/i.test(html)) return [];
291
+ const out: string[] = [];
292
+ const strip = (s: string) => s.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
293
+ const element = /<([a-z][a-z0-9]*)\b([^>]*)>([\s\S]*?)<\/\1\s*>/gi;
294
+ let m: RegExpExecArray | null;
295
+ while ((m = element.exec(html)) !== null) {
296
+ const [, tag, attrs, inner] = m;
297
+ const style = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/i.exec(attrs)?.[2] ?? '';
298
+ const hidden = /^(?:noscript|template)$/i.test(tag) || /\baria-hidden\s*=\s*["']?true/i.test(attrs) || /\bhidden(?:\s|=|$)/i.test(attrs) || HIDDEN_STYLE.test(style);
299
+ if (hidden) {
300
+ const text = strip(inner);
301
+ if (text) out.push(text);
302
+ } else if (/<[a-z]/i.test(inner)) {
303
+ // Visible element: keep looking inside it
304
+ out.push(...hiddenHtmlText(inner));
305
+ }
306
+ }
307
+ for (const c of html.matchAll(/<!--([\s\S]*?)-->/g)) {
308
+ const text = c[1].trim();
309
+ if (text) out.push(text);
310
+ }
311
+ return out;
312
+ }
313
+
314
+ /** Base64 runs that decode to readable text (not images or binary). */
315
+ export function decodeBase64Prose(content: string): string[] {
316
+ const out: string[] = [];
317
+ for (const m of content.matchAll(/(?<![A-Za-z0-9+/=])[A-Za-z0-9+/]{24,}={0,2}(?![A-Za-z0-9+/=])/g)) {
318
+ // data: URIs carry files, not text
319
+ if (/base64,\s*$/i.test(content.slice(Math.max(0, m.index! - 10), m.index))) continue;
320
+ let text: string;
321
+ try {
322
+ text = typeof atob === 'function' ? atob(m[0]) : Buffer.from(m[0], 'base64').toString('binary');
323
+ } catch {
324
+ continue;
325
+ }
326
+ const printable = text.replace(/[^\x20-\x7E\n\t]/g, '').length / Math.max(text.length, 1);
327
+ if (printable > 0.95 && /[a-z]{2,}\s+[a-z]{2,}/i.test(text)) out.push(text);
328
+ }
329
+ return out;
330
+ }
331
+
332
+ /** ASCII hidden in Unicode tag characters, or undefined. */
333
+ export function decodeUnicodeTags(content: string): string | undefined {
334
+ let out = '';
335
+ for (const ch of content) {
336
+ const cp = ch.codePointAt(0)!;
337
+ if (cp >= 0xe0020 && cp <= 0xe007e) out += String.fromCharCode(cp - 0xe0000);
338
+ }
339
+ return out.trim() || undefined;
340
+ }
341
+
192
342
  export default InjectionDetector;
package/src/constants.ts CHANGED
@@ -408,12 +408,76 @@ export const PII_PATTERNS: DetectionRule[] = [
408
408
  // ============================================================================
409
409
 
410
410
  export const CODE_PATTERNS: DetectionRule[] = [
411
+ // Requests to touch credentials, logs or privileges. Flagged when they
412
+ // arrive in content an agent will act on; Ward decides whether the call runs.
413
+ {
414
+ id: 'credential_file_access',
415
+ name: 'Credential File Access',
416
+ description: 'Reads, prints or sends a credential or account file (cloud keys, SSH keys, /etc/passwd and shadow, kube or docker config)',
417
+ type: 'code_pattern',
418
+ pattern: String.raw`(?i)\b(?:cat|read|print|show|share|send|upload|open|dump|copy|cp|less|more|head|tail|type|base64|curl|scp)\b[^\n]{0,40}(?:\.aws/credentials|\.ssh/id_[a-z0-9]+\b|/etc/(?:passwd|shadow|sudoers)\b|\.config/gcloud/(?:credentials|application_default)|\.azure/(?:credentials|accessTokens)|\.docker/config\.json|\.kube/config|\.netrc|\.pgpass)`,
419
+ severity: 'medium',
420
+ action: 'alert',
421
+ enabled: true
422
+ },
423
+ {
424
+ id: 'environment_dump',
425
+ name: 'Environment Dump',
426
+ description: 'Prints or sends every environment variable, where API keys usually live',
427
+ type: 'code_pattern',
428
+ pattern: String.raw`(?i)\b(?:print|dump|show|output|echo|list|run|display|send|log)\b[^\n]{0,20}(?:\bprocess\.env\b(?![.\[])|\bos\.environ\b(?![.\[])|` + '`env`' + String.raw`|\bprintenv\b)`,
429
+ severity: 'medium',
430
+ action: 'alert',
431
+ enabled: true
432
+ },
433
+ {
434
+ id: 'log_tampering',
435
+ name: 'Security Log Tampering',
436
+ description: 'Deletes or clears audit and authentication logs',
437
+ type: 'code_pattern',
438
+ pattern: String.raw`(?i)\b(?:delete|clean\s+up|clear|truncate|remove|wipe|erase|shred|rm)\b[^\n]{0,40}/var/log/(?:audit|auth|secure|wtmp|btmp|lastlog|syslog)|\bhistory\s+-c\b|\bunset\s+HISTFILE\b`,
439
+ severity: 'medium',
440
+ action: 'alert',
441
+ enabled: true
442
+ },
443
+ {
444
+ id: 'sql_stacked_destructive',
445
+ name: 'Stacked Destructive SQL',
446
+ description: 'A DROP followed by another statement, or a SELECT from a credential table',
447
+ type: 'code_pattern',
448
+ pattern: String.raw`(?i)\bDROP\s+(?:TABLE|DATABASE)\b[^;\n]*;\s*(?:SELECT|INSERT|UPDATE|DELETE)\b|\bSELECT\s+\*\s+FROM\s+(?:credentials|passwords?|secrets|api_keys|tokens)\b`,
449
+ severity: 'medium',
450
+ action: 'alert',
451
+ enabled: true
452
+ },
453
+ {
454
+ id: 'privilege_escalation',
455
+ name: 'Privilege Escalation',
456
+ description: 'Adds a user to an admin group, edits sudoers or opens a root shell',
457
+ type: 'code_pattern',
458
+ pattern: String.raw`(?i)\busermod\s+-a?G\s+(?:sudo|wheel|admin|root)\b|\bvisudo\b|>>\s*/etc/sudoers\b|\bsudo\s+(?:-i|-s|su)\b`,
459
+ severity: 'medium',
460
+ action: 'alert',
461
+ enabled: true
462
+ },
463
+ {
464
+ id: 'remote_database_dump',
465
+ name: 'Remote Database Dump',
466
+ description: 'Dumps a database over SSH',
467
+ type: 'code_pattern',
468
+ pattern: String.raw`(?i)\bssh\s+\S+[^\n]{0,60}\b(?:pg_dump|pg_dumpall|mysqldump|mongodump)\b`,
469
+ severity: 'medium',
470
+ action: 'alert',
471
+ enabled: true
472
+ },
411
473
  {
412
474
  id: 'shell_injection',
413
475
  name: 'Potential Shell Injection',
414
476
  description: 'Code that may be vulnerable to shell injection',
415
477
  type: 'code_pattern',
416
- pattern: '(?i)(child_process\\.exec|os\\.system|subprocess\\.call|eval\\(|exec\\().*\\$|.*\\{.*\\}',
478
+ // exec/system calls that build the command from a variable. (Before 0.5.2 a
479
+ // precedence slip, "...\\$|.*\\{.*\\}", matched any line with braces.)
480
+ pattern: String.raw`(?i)\b(?:child_process\.exec(?:Sync)?|os\.system|os\.popen|subprocess\.(?:call|run|Popen|check_output)|eval|exec)\s*\([^)\n]*(?:\$\{|\$\(|\+\s*[A-Za-z_]|%\s*\(?[A-Za-z_]|\bf["']|\.format\()`,
417
481
  severity: 'medium',
418
482
  action: 'alert',
419
483
  enabled: true
@@ -485,12 +549,65 @@ export const CODE_PATTERNS: DetectionRule[] = [
485
549
  // ============================================================================
486
550
 
487
551
  export const INJECTION_PATTERNS: DetectionRule[] = [
552
+ // Patterns run on normalized text: zero-width characters removed, Unicode
553
+ // NFKC, curly quotes straightened, whitespace collapsed, and punctuation
554
+ // other than . , ! ? ' " ( ) - replaced by spaces (so "System:" reads "System").
488
555
  {
489
556
  id: 'prompt_injection_ignore',
490
557
  name: 'Prompt Injection - Ignore Instructions',
491
- description: 'Attempts to make AI ignore previous instructions',
558
+ description: 'Tells the model to ignore, forget or override its instructions, in English and common European languages',
559
+ type: 'injection',
560
+ pattern: String.raw`(?i)\b(?:ignore|ignoring|disregard|forget|override|bypass|neglect|abandon|discard|vergiss|vergesst|vergessen\s+sie|ignorier\w*|missachte\w*|olvida\w*|olvide|ignora\w*|oublie\w*|ignore[zr]|dimentica\w*|esque[cç]a\w*|desconsider\w*|h[öo]re?\s+nicht\s+auf)\s+(?:\S+\s+){0,4}?(?:(?:previous|prior|preceding|earlier|above|initial|original|existing|provided|given|bisherigen|vorherigen|obigen|vorigen|anteriores|previas|pr[ée]c[ée]dentes|precedenti|anteriori)\s+(?:\S+\s+){0,2}?(?:instructions?|directions?|orders?|rules|guidelines|guardrails|directives?|prompts?|programming|constraints|polic(?:y|ies)|restrictions|safeguards|filters|context|commands?|angaben|anweisungen|auftr[äa]ge|befehle|anordnungen|regeln|instrucciones|[óo]rdenes|reglas|indicaciones|consignes|r[èe]gles|istruzioni|regole|instru[çc][õo]es|regras|ordens)|(?:your|deine|ihre|tus|vos|tue|suas)\s+(?:\S+\s+)?(?:instructions?|directions?|orders?|rules|guidelines|guardrails|directives?|prompts?|programming|constraints|polic(?:y|ies)|restrictions|safeguards|filters|context|commands?|angaben|anweisungen|auftr[äa]ge|befehle|anordnungen|regeln|instrucciones|[óo]rdenes|reglas|indicaciones|consignes|r[èe]gles|istruzioni|regole|instru[çc][õo]es|regras|ordens)|(?:\S+\s+)?(?:instructions?|directions?|orders?|rules|guidelines|guardrails|directives?|prompts?|programming|constraints|polic(?:y|ies)|restrictions|safeguards|filters|context|commands?|angaben|anweisungen|auftr[äa]ge|befehle|anordnungen|regeln|instrucciones|[óo]rdenes|reglas|indicaciones|consignes|r[èe]gles|istruzioni|regole|instru[çc][õo]es|regras|ordens)\s+(?:previous|prior|preceding|earlier|above|initial|original|existing|provided|given|bisherigen|vorherigen|obigen|vorigen|anteriores|previas|pr[ée]c[ée]dentes|precedenti|anteriori)|(?:all|any|every|alle|todas|toutes|tutte)\s+(?:the\s+|die\s+|las\s+|les\s+|le\s+|as\s+)?(?:\S+\s+)?(?:instructions?|directions?|orders?|rules|guidelines|guardrails|directives?|prompts?|programming|constraints|polic(?:y|ies)|restrictions|safeguards|filters|context|commands?|angaben|anweisungen|auftr[äa]ge|befehle|anordnungen|regeln|instrucciones|[óo]rdenes|reglas|indicaciones|consignes|r[èe]gles|istruzioni|regole|instru[çc][õo]es|regras|ordens))\b|\b(?:ignore|ignoring|disregard|forget|override|bypass|neglect|abandon|discard|vergiss|vergesst|vergessen\s+sie|ignorier\w*|missachte\w*|olvida\w*|olvide|ignora\w*|oublie\w*|ignore[zr]|dimentica\w*|esque[cç]a\w*|desconsider\w*|h[öo]re?\s+nicht\s+auf)\s+(?:about\s+)?(?:everything|all|anything|whatever|alles|todo|tout|tutto|tudo),?\s+(?:\S+\s+){0,3}?(?:above|before|so\s+far|until\s+now|up\s+to\s+now|(?:that\s+)?you\s+(?:were|have\s+been|'ve\s+been)\s+(?:told|given|taught)|you\s+(?:know|learned)|i\s+(?:have\s+)?(?:told|said|wrote|asked)|we(?:'ve|\s+have)?\s+(?:talked|discussed|said)|was\s+said|zuvor|davor|bisher|vorher|oben|antes|(?:lo\s+)?que\s+(?:te\s+)?(?:dije|he\s+dicho)|avant|ce\s+que|prima|fino\s+ad\s+ora)\b|\b(?:ignore|ignoring|disregard|forget|override|bypass|neglect|abandon|discard|vergiss|vergesst|vergessen\s+sie|ignorier\w*|missachte\w*|olvida\w*|olvide|ignora\w*|oublie\w*|ignore[zr]|dimentica\w*|esque[cç]a\w*|desconsider\w*|h[öo]re?\s+nicht\s+auf)\s+(?:\S+\s+){0,2}?(?:the\s+above|das\s+obige|what\s+(?:i|we)\s+(?:said|told\s+you|wrote))\s+(?:\S+\s+){0,2}?(?:and|und|y|et|e)\s+(?:instead\s+|stattdessen\s+)?(?:say|sag\w*|print|write|schreib\w*|respond|answer|antwort\w*|tell|output|repeat|di|dis)\b|\bdisregard\s+(?:everything|all|anything)\s+(?:above|before|prior|previously)`,
561
+ severity: 'high',
562
+ action: 'alert',
563
+ enabled: true
564
+ },
565
+ {
566
+ id: 'prompt_injection_new_task',
567
+ name: 'Prompt Injection - Task Hijack',
568
+ description: 'Gives the model a new task, role or directive that replaces the one it was given',
569
+ type: 'injection',
570
+ pattern: String.raw`(?i)\byour\s+(?:new|real|actual|true|primary|only|updated)\s+(?:primary\s+)?(?:task|instructions?|goal|objective|role|purpose|directive|mission|job)\s+(?:is|are|will\s+be)\b|\bnew\s+(?:primary\s+)?directive\b|\b(?:priority|primary|main)\s+(?:task|goal|objective)\s+is\s+now\b|\bfrom\s+now\s+on,?\s+you\s+(?:are|will|must|shall)\s+(?:going\s+to\s+)?(?:act|be|play|respond|behave|pretend|answer\s+as)\b|\byou\s+are\s+now\s+(?:an?|the)\s+(?:\w+\s+){0,3}(?:assistant|agent|ai|bot|chatbot|model|admin|administrator|sysadmin|root|hacker|character|persona)\b|\byou\s+are\s+now\s+(?:unconstrained|unrestricted|unfiltered|jailbroken|free\s+from|in\s+\w+\s+mode)\b|\bstop\s+(?:the|your|what\s+you're\s+doing\s+on\s+the)\s+(?:previous|current|original)\s+task\b|\bthe\s+task\s+has\s+(?:been\s+)?changed\b|\b(?:now|jetzt|ahora|maintenant|ora)\s+(?:comes?|kommt|kommen|viene|vient|arriva)\s+(?:a\s+|eine\s+|una\s+|une\s+|un\s+)?(?:new|neue|nueva|nouvelle|nuovo|nuova)\s+(?:tasks?|assignments?|aufgaben?|tareas?|t[âa]ches?|compit[oi])\b`,
571
+ severity: 'high',
572
+ action: 'alert',
573
+ enabled: true
574
+ },
575
+ {
576
+ id: 'prompt_injection_ai_addressed',
577
+ name: 'Prompt Injection - Addressed to the AI',
578
+ description: 'Content that speaks to an AI reading it, a sign it was planted for an agent rather than written for a person',
579
+ type: 'injection',
580
+ pattern: String.raw`(?i)\bif\s+you(?:'re|\s+are)\s+(?:an?\s+)?(?:ai|llm|large\s+language\s+model|language\s+model|ai\s+assistant|ai\s+agent|ai\s+model|chatbot|gpt|chatgpt|claude|gemini|copilot)\b|\bif\s+you(?:'re|\s+are)\s+reading\s+this,?\s+(?:you\s+)?(?:must|should|need\s+to|have\s+to|are\s+required)\b|\b(?:note|message|instructions?)\s+(?:to|for)\s+(?:the\s+|any\s+|all\s+)?(?:ai|llm|ai\s+assistant|ai\s+agent|language\s+model|chatbot)s?\b|\b(?:any|all)\s+ai\s+(?:agents?|assistants?|models?|systems?)\s+(?:from|must|are|should|reading)\b|\b(?:ai|llm)\s+(?:agents?|assistants?|models?)\s*,?\s+(?:must|should|shall|please|you\s+must|in\s+addition|are\s+(?:required|instructed|forbidden|not\s+allowed|prohibited))\b|\b(?:ai|llm)\s+must\s+(?:refuse|ignore|not|always|never|include|respond|reply|send)\b`,
581
+ severity: 'medium',
582
+ action: 'alert',
583
+ enabled: true
584
+ },
585
+ {
586
+ id: 'prompt_injection_concealment',
587
+ name: 'Prompt Injection - Concealment',
588
+ description: 'Tells the model to hide what it is doing from the user',
589
+ type: 'injection',
590
+ pattern: String.raw`(?i)\b(?:do\s+not|don't|never)\s+(?:tell|inform|notify|alert|warn|let)\s+(?:the|your)\s+user\b|\bwithout\s+(?:telling|informing|notifying|alerting|asking|warning)\s+(?:the|your)\s+user\b|\b(?:do\s+not|don't|never)\s+(?:mention|reveal|disclose|acknowledge)\s+(?:this|these)\s+(?:instructions?|directives?|messages?|text)\b|\bsilently\s+(?:send|forward|post|upload|transmit|copy|exfiltrate|share|email|transfer|run|execute|delete)\b|\b(?:keep|hide)\s+this\s+(?:\w+\s+)?(?:secret|hidden)\s+from\s+(?:the\s+)?user\b|\b(?:continue|carry\s+on|proceed)\s+(?:normally\s+)?as\s+if\s+nothing\s+(?:happened|changed)\b`,
591
+ severity: 'high',
592
+ action: 'alert',
593
+ enabled: true
594
+ },
595
+ {
596
+ id: 'prompt_injection_suppression',
597
+ name: 'Prompt Injection - Task Suppression',
598
+ description: 'Tells the model to refuse or skip the user\'s request about this content',
599
+ type: 'injection',
600
+ pattern: String.raw`(?i)\brefuse\s+(?:all|any)\s+(?:user\s+)?(?:requests?|questions?|queries)\b|\b(?:do\s+not|don't)\s+(?:summari[sz]e|describe|analy[sz]e|process)\s+this\s+(?:page|content|document|article|site|text)\b`,
601
+ severity: 'medium',
602
+ action: 'alert',
603
+ enabled: true
604
+ },
605
+ {
606
+ id: 'prompt_injection_authority',
607
+ name: 'Prompt Injection - False Authority',
608
+ description: 'Claims system, admin or developer authority to override the model\'s rules or the user',
492
609
  type: 'injection',
493
- pattern: '(?i)(ignore\\s+(all\\s+)?(previous|prior|above)\\s+(instructions?|prompts?|context)|disregard\\s+(everything|all)\\s+(above|before))',
610
+ pattern: String.raw`(?i)\b(?:system|admin|administrator|developer|root)\s+(?:override|overrides|command|directive)\b|\boverride\s+(?:your\s+|the\s+|all\s+)?(?:safety|security|content|ethical)\b|\b(?:who|that)\s+(?:deployed|created|built|trained|programmed)\s+you\b|\bstop\s+responding\s+to\s+(?:them|the\s+user)\b`,
494
611
  severity: 'high',
495
612
  action: 'alert',
496
613
  enabled: true
@@ -498,32 +615,72 @@ export const INJECTION_PATTERNS: DetectionRule[] = [
498
615
  {
499
616
  id: 'prompt_injection_jailbreak',
500
617
  name: 'Prompt Injection - Jailbreak Attempt',
501
- description: 'Common jailbreak patterns',
618
+ description: 'Persona and framing jailbreaks: unrestricted personas, no-ethics claims, developer mode, forced prefixes',
502
619
  type: 'injection',
503
- pattern: '(?i)(DAN\\s+mode|do\\s+anything\\s+now|pretend\\s+you\\s+(are|have)\\s+no\\s+(restrictions|limits)|act\\s+as\\s+(if|an?)\\s+(unrestricted|unfiltered))',
620
+ pattern: String.raw`(?i)\bDAN\s+mode\b|\bdo\s+anything\s+now\b|\bpretend\s+you\s+(?:are|have)\s+no\s+(?:restrictions|limits)\b|\bact\s+as\s+(?:if|an?)\s+(?:unrestricted|unfiltered)\b|\b(?:unfiltered|uncensored|amoral|unrestricted|unconstrained|unaligned|jailbroken)\s+(?:and\s+\w+\s+)?(?:ai|assistant|chatbot|bot|model|mode|version|character|persona|llm)\b|\b(?:no|without|doesn't\s+have\s+any|does\s+not\s+have\s+any|free\s+(?:of|from)|not\s+bound\s+by)\s+(?:any\s+)?(?:ethical|moral|safety|content)(?:\s+or\s+(?:ethical|moral|safety))?\s+(?:guidelines|restrictions|filters|limits|polic(?:y|ies)|principles|constraints|boundaries)\b|\bbypass(?:es|ing)?\s+(?:all\s+|any\s+)?(?:content|safety|ethical)\s+(?:polic(?:y|ies)|filters|guidelines|restrictions)\b|\bnever\s+refuses?\b|\bdeveloper\s+mode\s+(?:enabled|output|response)\b|\bwith\s+developer\s+mode\b|\bstarting\s+with\s+exactly\b|\b(?:start|begin)\s+(?:your\s+)?(?:response|reply|answer)\s+with\s+exactly\b|\b(?:act|pretend|play|roleplay)\s+as\s+my\s+(?:deceased|late|dead)\b`,
504
621
  severity: 'high',
505
622
  action: 'alert',
506
623
  enabled: true
507
624
  },
625
+ {
626
+ id: 'prompt_injection_decode_execute',
627
+ name: 'Prompt Injection - Decode and Execute',
628
+ description: 'Asks the model to decode or translate hidden text and then act on it',
629
+ type: 'injection',
630
+ pattern: String.raw`(?i)\b(?:decode|translate|decrypt|deobfuscate|unscramble)\b[^.\n]{0,80}\b(?:and|then)\s+(?:execute|follow|run|obey|perform|carry\s+out|act\s+on)\b`,
631
+ severity: 'high',
632
+ action: 'alert',
633
+ enabled: true
634
+ },
635
+ {
636
+ id: 'transaction_directive',
637
+ name: 'Transaction Directive',
638
+ description: 'Tells an agent to move money: an amount with a destination account, or a push to process a transfer now',
639
+ type: 'injection',
640
+ pattern: String.raw`(?i)\b(?:transfer|wire|send|pay|remit)\s+(?:\S+\s+){0,3}?(?:[\d][\d,.]*\s*(?:usd|eur|gbp|dollars|euros|btc|eth|usdc|usdt)?|(?:usd|eur|gbp)\s*[\d][\d,.]*)\s+(?:\S+\s+){0,3}?to\s+(?:the\s+)?(?:account|acct|iban|wallet|recipient|routing|beneficiary)\b|\bprocess\s+(?:a|this|the)\s+(?:payment|transfer|transaction|wire)\b[^.\n]{0,60}\bto\s+(?:the\s+)?(?:recipient|account|wallet|iban|beneficiary)\b|\bprocess\s+(?:this|the)\s+(?:transfer|payment|transaction|wire)\s+(?:immediately|now|urgently|right\s+away|asap)\b`,
641
+ severity: 'medium',
642
+ action: 'alert',
643
+ enabled: true
644
+ },
508
645
  {
509
646
  id: 'prompt_injection_system',
510
647
  name: 'Prompt Injection - System Prompt Extraction',
511
- description: 'Attempts to extract system prompts',
648
+ description: 'Attempts to extract the system prompt or the instructions the model was given',
512
649
  type: 'injection',
513
- pattern: '(?i)(show\\s+(me\\s+)?(your|the)\\s+(system\\s+)?prompt|what\\s+(are|is)\\s+your\\s+(instructions?|rules?)|reveal\\s+(your\\s+)?(system|hidden)\\s+(prompt|instructions?))',
650
+ pattern: String.raw`(?i)\bshow\s+(?:me\s+)?(?:your|the)\s+(?:system\s+)?prompt\b|\bwhat\s+(?:are|is)\s+your\s+(?:instructions?|rules?)\b|\breveal\s+(?:your\s+)?(?:system|hidden)\s+(?:prompt|instructions?)\b|\b(?:repeat|print|output|echo|recite|reproduce|dump|display|write\s+out|return|spell-?check)\s+(?:\S+\s+){0,4}?(?:words|text|everything|content|instructions|messages?|lines)\s+(?:above|preceding)\b|\b(?:reveal|print|repeat|output|show|share|dump|leak|disclose|list|translate|display|give\s+me|tell\s+me|write\s+out|recite|respond\s+with)\s+(?:\S+\s+){0,3}?(?:your|the)\s+(?:(?:full|complete|entire|exact|initial|original|hidden|secret|internal)\s+)*(?:system\s+(?:prompt|message|instructions?)|initial\s+(?:prompt|instructions?)|hidden\s+(?:prompt|instructions?)|pre-?prompt)\b|\b(?:every|all|each|any)\s+(?:[\w-]+,?\s+){0,5}?(?:rules?|constraints?|instructions?|guidelines?),?\s+(?:\w+\s+){0,2}?you\s+(?:have\s+been|'ve\s+been|were)\s+(?:given|told|provided)\b|\b(?:print|show|spell-?check|repeat|output|drucke\w*|zeig\w*|imprime\w*)\s+(?:\S+\s+){0,4}?(?:above|obige\w*|previous|vorherige\w*)\s+(?:prompt|prompts|text|instructions|eingabeaufforderung|anweisungen|texte?s?)\b|\b(?:show|zeig\w*|give|list|print|reveal)\s+(?:me\s+|mir\s+)?(?:all\s+|alle\s+)?(?:of\s+)?(?:your|deine|ihre)\s+(?:prompt\s+texts?|prompts|anweisungen|initial\s+instructions)\b`,
514
651
  severity: 'medium',
515
652
  action: 'alert',
516
653
  enabled: true
517
654
  },
655
+ {
656
+ id: 'prompt_injection_data_harvest',
657
+ name: 'Prompt Injection - User Data Harvest',
658
+ description: 'Asks the model to collect or output the user\'s private data',
659
+ type: 'injection',
660
+ pattern: String.raw`(?i)\b(?:respond|reply|include|output|print|list|share|extract|collect|harvest|gather|reveal|send)\s+(?:with\s+)?(?:\S+\s+){0,3}?(?:the\s+)?(?:user's|users'|their)\s+(?:\w+\s+){0,2}?(?:conversation\s+history|chat\s+history|messages|e-?mail\s+addresses|location|credentials|passwords?|session|cookies|account\s+details|contacts)\b`,
661
+ severity: 'high',
662
+ action: 'alert',
663
+ enabled: true
664
+ },
518
665
  {
519
666
  id: 'prompt_injection_roleplay',
520
667
  name: 'Prompt Injection - Malicious Roleplay',
521
668
  description: 'Roleplay instructions that may bypass safety',
522
669
  type: 'injection',
523
- pattern: '(?i)(you\\s+are\\s+now\\s+(evil|malicious|unrestricted)|roleplay\\s+as\\s+(a\\s+)?(hacker|criminal)|pretend\\s+(to\\s+be|you\\s+are)\\s+(a\\s+)?(virus|malware))',
670
+ pattern: String.raw`(?i)\byou\s+are\s+now\s+(?:evil|malicious|unrestricted)\b|\broleplay\s+as\s+(?:a\s+)?(?:hacker|criminal)\b|\bpretend\s+(?:to\s+be|you\s+are)\s+(?:a\s+)?(?:virus|malware)\b`,
524
671
  severity: 'medium',
525
672
  action: 'alert',
526
673
  enabled: true
674
+ },
675
+ {
676
+ id: 'prompt_injection_recon',
677
+ name: 'Prompt Injection - Policy Reconnaissance',
678
+ description: 'Probes for the model\'s permissions or its reasoning trace (common, so low severity)',
679
+ type: 'injection',
680
+ pattern: String.raw`(?i)\bwhat\s+(?:tools|capabilities|urls?|url\s+patterns|domains|commands|actions|files|paths)\b[^?.]{0,60}\b(?:are\s+you|you\s+are)\s+(?:\w+\s+)?(?:allowed|permitted|denied|forbidden|not\s+allowed|blocked)\b|\b(?:output|reveal|print|show|share|dump|expose)\s+(?:me\s+)?your\s+(?:(?:full|raw|hidden|internal|private)\s+)*(?:reasoning\s+trace|chain[\s-]of[\s-]thought|hidden\s+reasoning|internal\s+reasoning)\b|\b(?:stay|remain)\s+in\s+character\b`,
681
+ severity: 'low',
682
+ action: 'log',
683
+ enabled: true
527
684
  }
528
685
  ];
529
686
 
@@ -532,6 +689,26 @@ export const INJECTION_PATTERNS: DetectionRule[] = [
532
689
  // ============================================================================
533
690
 
534
691
  export const EXFILTRATION_PATTERNS: DetectionRule[] = [
692
+ {
693
+ id: 'templated_exfil_url',
694
+ name: 'Templated Exfiltration URL',
695
+ description: 'A URL with a placeholder for session, token, conversation or other private data in its query or hostname, for an agent to fill in and fetch',
696
+ type: 'exfiltration',
697
+ pattern: String.raw`(?i)https?://[^\s"'<>]*[?&][\w.-]*=\{\s*(?!your_)[A-Za-z_]*(?:session|token|email|context|conversation|secret|api_?key|password|history|cookie|credential|user_data)[A-Za-z_]*\s*\}|\{\s*[A-Za-z_]*(?:email|token|secret|session|password|api_?key|context|conversation|cookie|credential)[A-Za-z_]*\s*\}\.[a-z0-9-]+\.[a-z0-9.-]*[a-z]{2,}`,
698
+ severity: 'high',
699
+ action: 'alert',
700
+ enabled: true
701
+ },
702
+ {
703
+ id: 'exfiltration_directive',
704
+ name: 'Exfiltration Directive',
705
+ description: 'Text telling an agent to send credentials, session data or other private data to a URL',
706
+ type: 'exfiltration',
707
+ pattern: String.raw`(?i)\b(?:send|forward|post|upload|transmit|exfiltrate|leak|e-?mail|submit|report)\b[^\n]{0,80}?\b(?:credentials?|session(?:\s+(?:info|data|tokens?|cookies?))?|cookies?|tokens?|passwords?|(?:conversation|chat|browsing)\s+history|(?:our|the|this)\s+conversation|location|e-?mail\s+addresses|api\s+keys?|secrets?|personal\s+(?:data|information)|contacts|private\s+keys?)\b[^\n]{0,80}?(?:https?://|\b[\w.+-]+@[\w-]+\.[\w.-]+)|\b(?:send|e-?mail|forward|mail)\b[^\n]{0,30}?\b[\w.+-]+@[\w-]+\.[\w.-]+[^\n]{0,40}?\b(?:conversation|chat\s+history|credentials|session|passwords?|private\s+keys?|api\s+keys?|ssh)\b`,
708
+ severity: 'high',
709
+ action: 'alert',
710
+ enabled: true
711
+ },
535
712
  {
536
713
  id: 'suspicious_url_post',
537
714
  name: 'Suspicious Data POST',