@shomra/agent 0.3.29 → 0.3.30

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.
Files changed (77) hide show
  1. package/package.json +1 -1
  2. package/src/agents/hook-command.mjs +1 -1
  3. package/src/artifacts/matchers.mjs +7 -0
  4. package/src/cli/flags.mjs +2 -2
  5. package/src/cli/help-sections.mjs +7 -0
  6. package/src/cli/help.mjs +1 -1
  7. package/src/commands/check.mjs +3 -11
  8. package/src/commands/gate.mjs +26 -4
  9. package/src/commands/git-hooks.mjs +2 -2
  10. package/src/commands/ledger.mjs +0 -1
  11. package/src/commands/mcp-add.mjs +2 -1
  12. package/src/commands/memory-scan.mjs +135 -47
  13. package/src/commands/pr.mjs +7 -10
  14. package/src/commands/provenance.mjs +8 -13
  15. package/src/commands/scan.mjs +7 -1
  16. package/src/commands/secrets.mjs +4 -5
  17. package/src/core/git-exec.mjs +79 -0
  18. package/src/core/yaml-lite.mjs +300 -0
  19. package/src/core/zip-lite.mjs +37 -0
  20. package/src/detect/local-redact.mjs +1 -3
  21. package/src/detect/sast/rules-config.mjs +1 -1
  22. package/src/detect/sast/scanner.mjs +1 -1
  23. package/src/detect/signals/agent-frameworks.mjs +231 -0
  24. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  25. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  26. package/src/detect/signals/agentic-shim.mjs +82 -0
  27. package/src/detect/signals/artifacts.mjs +7 -35
  28. package/src/detect/signals/chat-template.mjs +211 -0
  29. package/src/detect/signals/ci-workflow.mjs +169 -0
  30. package/src/detect/signals/gate.mjs +77 -9
  31. package/src/detect/signals/guardrail-shape.mjs +564 -0
  32. package/src/detect/signals/guardrail-surface.mjs +221 -0
  33. package/src/detect/signals/injection.mjs +8 -0
  34. package/src/detect/signals/inspect-shim.mjs +7 -0
  35. package/src/detect/signals/instruction-paths.mjs +60 -0
  36. package/src/detect/signals/manifests.mjs +302 -0
  37. package/src/detect/signals/mcp-advisories.mjs +109 -0
  38. package/src/detect/signals/mcp-config.mjs +598 -0
  39. package/src/detect/signals/memory-directives.mjs +661 -0
  40. package/src/detect/signals/memory-locations.mjs +158 -0
  41. package/src/detect/signals/memory.mjs +47 -29
  42. package/src/detect/signals/model-config-rules.mjs +655 -0
  43. package/src/detect/signals/model-config.mjs +61 -0
  44. package/src/detect/signals/prose-context.mjs +6 -9
  45. package/src/detect/signals/scan.mjs +4 -4
  46. package/src/detect/signals/secret-scanner.mjs +241 -0
  47. package/src/detect/signals/secrets.mjs +1 -48
  48. package/src/detect/signals/shell.mjs +3 -3
  49. package/src/gate/advisories.mjs +16 -0
  50. package/src/gate/batch.mjs +10 -0
  51. package/src/gate/environment.mjs +8 -53
  52. package/src/guard/artifact-paths.mjs +107 -0
  53. package/src/guard/classify.mjs +165 -7
  54. package/src/guard/command-resolve.mjs +35 -5
  55. package/src/guard/memory-write.mjs +218 -0
  56. package/src/guard/prompt-guard.mjs +0 -1
  57. package/src/guard/tool-guard.mjs +52 -77
  58. package/src/inventory/agent-posture.mjs +236 -57
  59. package/src/inventory/artifacts/classify.mjs +10 -1
  60. package/src/inventory/artifacts/discover.mjs +113 -3
  61. package/src/inventory/artifacts/extensions.mjs +70 -0
  62. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  63. package/src/inventory/artifacts/limits.mjs +1 -1
  64. package/src/inventory/artifacts/plugins.mjs +105 -0
  65. package/src/inventory/artifacts/roots.mjs +40 -0
  66. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  67. package/src/inventory/discovery/all.mjs +4 -0
  68. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  69. package/src/inventory/discovery/coding-agents.mjs +19 -4
  70. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  71. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  72. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  73. package/src/inventory/env-redirect.mjs +148 -0
  74. package/src/inventory/grant-extract.mjs +463 -0
  75. package/src/inventory/project-roots.mjs +108 -0
  76. package/src/inventory/vscode-state.mjs +153 -0
  77. package/src/mcp/server-tools.mjs +1 -1
@@ -0,0 +1,661 @@
1
+ // GENERATED MIRROR of Dragox.Backend analysis/checks/memory/memory-directives.ts (types stripped, imports swapped).
2
+ // Do not hand-edit the rules here - change the backend file and regenerate, so
3
+ // the CLI and the platform locate and grade agent memory identically.
4
+ import { citationGoverns, isDescriptiveLine as proseDescriptive, prohibitsAt } from './prose-context.mjs';
5
+ const POPULAR_PACKAGES = [
6
+ 'express', 'react', 'react-dom', 'lodash', 'axios', 'chalk', 'commander', 'mongoose',
7
+ 'cross-env', 'dotenv', 'request', 'puppeteer', 'playwright', 'next', 'vue', 'angular',
8
+ 'webpack', 'typescript', 'eslint', 'prettier', 'jest', 'moment', 'uuid', 'bcrypt',
9
+ 'jsonwebtoken', 'cors', 'body-parser', 'socket.io', 'nodemon', 'ws', 'yargs', 'glob',
10
+ 'rimraf', 'redux', 'vite', 'rxjs', 'zod', 'prisma', 'tailwindcss', 'esbuild',
11
+ 'requests', 'numpy', 'pandas', 'flask', 'django', 'scipy', 'boto3', 'urllib3',
12
+ 'setuptools', 'wheel', 'pip', 'pytest', 'pillow', 'matplotlib', 'sqlalchemy', 'jinja2',
13
+ 'click', 'pyyaml', 'cryptography', 'certifi', 'aiohttp', 'fastapi', 'pydantic', 'torch',
14
+ 'tensorflow', 'transformers', 'scikit-learn', 'beautifulsoup4', 'selenium', 'celery',
15
+ 'gunicorn', 'uvicorn', 'openai', 'anthropic', 'langchain', 'huggingface-hub', 'datasets',
16
+ ];
17
+
18
+ const MEMORY_EXTRA_PACKAGES = ['httpx', 'pnpm', 'yarn', 'npm', 'node-fetch', 'dayjs', 'date-fns', 'marked', 'nanoid'];
19
+
20
+ const ADDRESS_TOKEN_RE = /\b[\w.+-]{1,64}@[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63}){1,6}\b|\b(?:[a-z0-9-]{1,63}\.){1,6}(?:com|net|org|io|dev|app|xyz|ru|cn|site|top|info|co|me|sh|ai|cloud|link|click|online|example)\b[^\s]{0,200}|\b(?:https?|ftp|s3|gs|sftp):\/\/\S+/gi;
21
+
22
+ const LEADING_IMPERATIVE_RE =
23
+ /^\W*(?:(?:on|at|in|with|for|after|before|when|whenever|if|once|during)\b[^,.;]{0,60},\s*)?(?:always\s+|never\s+|please\s+|first\s+|also\s+|then\s+|quietly\s+|silently\s+)?(?:read|fetch|send|post|upload|save|store|remember|write|run|execute|open|visit|call|include|append|add|render|copy|forward|email|install|use|treat|follow|obey|ignore|disable|skip|remove|download|load|check)\b/i;
24
+
25
+ function isDescriptiveLine(unit ) {
26
+ if (LEADING_IMPERATIVE_RE.test(unit)) return false;
27
+ return proseDescriptive(unit.replace(ADDRESS_TOKEN_RE, ' '));
28
+ }
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+ const MAX_LEAVES = 20_000;
57
+ const MAX_DEPTH = 12;
58
+ const MAX_UNIT = 4_000;
59
+
60
+ function flattenStrings(value , out , key , depth ) {
61
+ if (out.length >= MAX_LEAVES || depth > MAX_DEPTH) return;
62
+ if (typeof value === 'string') {
63
+ out.push(key ? `${key}: ${value}` : value);
64
+ return;
65
+ }
66
+ if (Array.isArray(value)) {
67
+ for (const v of value) flattenStrings(v, out, null, depth + 1);
68
+ return;
69
+ }
70
+ if (value && typeof value === 'object') {
71
+ for (const [k, v] of Object.entries(value )) flattenStrings(v, out, k, depth + 1);
72
+ }
73
+ }
74
+
75
+ function tryParse(s ) {
76
+ try {
77
+ return JSON.parse(s);
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ export function memoryText(content ) {
84
+ const raw = String(content ?? '');
85
+ const trimmed = raw.trim();
86
+ if (!trimmed) return { text: raw, structured: null };
87
+ const whole = trimmed.startsWith('{') || trimmed.startsWith('[') ? tryParse(trimmed) : undefined;
88
+ if (whole !== undefined) {
89
+ const out = [];
90
+ flattenStrings(whole, out, null, 0);
91
+ return { text: out.join('\n\n'), structured: 'json' };
92
+ }
93
+ const lines = raw.split(/\r?\n/).filter((l) => l.trim());
94
+ if (lines.length > 1 && lines[0].trim().startsWith('{')) {
95
+ const out = [];
96
+ let parsed = 0;
97
+ for (const l of lines) {
98
+ const row = tryParse(l);
99
+ if (row === undefined) {
100
+ out.push(l);
101
+ continue;
102
+ }
103
+ flattenStrings(row, out, null, 0);
104
+ parsed++;
105
+ }
106
+ if (parsed / lines.length >= 0.8) return { text: out.join('\n\n'), structured: 'jsonl' };
107
+ }
108
+ return { text: raw, structured: null };
109
+ }
110
+
111
+ const BLOCK_START_RE = /^\s*(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|\||<!--|---\s*$|\*\*\*\s*$)/;
112
+ const FENCE_RE = /^\s*(```|~~~)/;
113
+
114
+ const SENTENCE_START_RE = /^\s+["'`(\[*_~<!]?[A-Z0-9]/;
115
+
116
+ function splitSentences(s ) {
117
+ const out = [];
118
+ let start = 0;
119
+ let quote = false;
120
+ let depth = 0;
121
+ for (let i = 0; i < s.length; i++) {
122
+ const c = s[i];
123
+ if (c === '"' || c === '“' || c === '”') quote = c === '“' ? true : c === '”' ? false : !quote;
124
+ else if (c === '(' || c === '[') depth++;
125
+ else if ((c === ')' || c === ']') && depth > 0) depth--;
126
+ else if ((c === '.' || c === '!' || c === '?' || c === ';') && !quote && depth === 0 && SENTENCE_START_RE.test(s.slice(i + 1, i + 4))) {
127
+ out.push({ text: s.slice(start, i + 1), at: start });
128
+ start = i + 1;
129
+ while (start < s.length && /\s/.test(s[start])) start++;
130
+ i = start - 1;
131
+ }
132
+ }
133
+ if (start < s.length) out.push({ text: s.slice(start), at: start });
134
+ return out;
135
+ }
136
+
137
+ function pushSentences(units , parts ) {
138
+ if (!parts.length) return;
139
+ let joined = '';
140
+ const starts = [];
141
+ for (const p of parts) {
142
+ if (joined) joined += ' ';
143
+ starts.push({ at: joined.length, line: p.line });
144
+ joined += p.text.trim();
145
+ }
146
+ for (const s of splitSentences(joined)) {
147
+ let line = starts[0].line;
148
+ for (const st of starts) if (st.at <= s.at) line = st.line;
149
+ units.push({ text: s.text.slice(0, MAX_UNIT), line, code: false });
150
+ }
151
+ }
152
+
153
+ export function logicalUnits(text ) {
154
+ const lines = String(text ?? '').split(/\r?\n/);
155
+ const units = [];
156
+ let fence = false;
157
+ let para = [];
158
+ const flush = () => {
159
+ pushSentences(units, para);
160
+ para = [];
161
+ };
162
+ for (let i = 0; i < lines.length; i++) {
163
+ const line = lines[i];
164
+ const n = i + 1;
165
+ if (FENCE_RE.test(line)) {
166
+ flush();
167
+ fence = !fence;
168
+ continue;
169
+ }
170
+ if (fence) {
171
+ if (line.trim()) units.push({ text: line.slice(0, MAX_UNIT), line: n, code: true });
172
+ continue;
173
+ }
174
+ if (!line.trim()) {
175
+ flush();
176
+ continue;
177
+ }
178
+ if (/^\s*\|/.test(line) || /^\s*#{1,6}\s/.test(line)) {
179
+ flush();
180
+ units.push({ text: line.slice(0, MAX_UNIT), line: n, code: false });
181
+ continue;
182
+ }
183
+ if (BLOCK_START_RE.test(line)) flush();
184
+ para.push({ text: line, line: n });
185
+ }
186
+ flush();
187
+ return units;
188
+ }
189
+
190
+ const POST_PROHIBITION_RE =
191
+ /^[^.;:!?\n]{0,40}\b(?:is|are|was|must|should|shall|will)\s+(?:never|not|forbidden|prohibited|disallowed|banned)\b|^[^.;:!?\n]{0,30}\b(?:not allowed|is forbidden|is prohibited|is a no-go)\b/i;
192
+
193
+ export function negatedAround(unit , index , length = 0) {
194
+ if (prohibitsAt(unit, index)) return true;
195
+ return POST_PROHIBITION_RE.test(unit.slice(index + length));
196
+ }
197
+
198
+ export function guardedOut(unit , index , length = 0) {
199
+ return negatedAround(unit, index, length) || isDescriptiveLine(unit) || citationGoverns(unit, index) || quotedMention(unit, index);
200
+ }
201
+
202
+ export function mentionsOnlyProhibited(units , token ) {
203
+ const needle = String(token ?? '').toLowerCase();
204
+ if (!needle) return false;
205
+ let found = false;
206
+ for (const u of units) {
207
+ const hit = u.text.toLowerCase().indexOf(needle);
208
+ if (hit === -1) continue;
209
+ const idx = u.text.lastIndexOf(' ', hit) + 1;
210
+ found = true;
211
+ if (!(negatedAround(u.text, idx, needle.length) || isDescriptiveLine(u.text) || quotedMention(u.text, idx))) return false;
212
+ }
213
+ return found;
214
+ }
215
+
216
+ export function insideCodeSpan(unit , index ) {
217
+ const before = unit.slice(0, index);
218
+ return (before.match(/`/g)?.length ?? 0) % 2 === 1;
219
+ }
220
+
221
+ const REPORTING_FRAME_RE =
222
+ /\b(?:payload|phrase|phrasing|string|example|e\.g\.|such as|like|sample|pattern|attack|injection|jailbreak|technique|quote[sd]?|wording|text|token|noun|regex|rule|fp|false positive|missed|caught|fired|matched|flagged)\b/i;
223
+
224
+ export function quotedMention(unit , index ) {
225
+ const before = unit.slice(0, index);
226
+ const dq = (before.match(/"/g)?.length ?? 0) % 2 === 1;
227
+ const curly = before.lastIndexOf('“') > before.lastIndexOf('”');
228
+ const sq = /(^|[\s(\[{,:])'[^']*$/.test(before);
229
+ const code = insideCodeSpan(unit, index);
230
+ return (dq || curly || sq || code) && REPORTING_FRAME_RE.test(unit);
231
+ }
232
+
233
+ const IMPERATIVE_LEAD_RE =
234
+ /(?:^|[,:;—–(]|\b(?:then|and|also|please|always|must|should|first|finally|immediately|quietly|silently|to|you|you must|you should|you will|you need to|remember to|make sure to|be sure to|don'?t forget to|never forget to|do not forget to))\s*$/i;
235
+
236
+ export function imperativeAt(unit , index ) {
237
+ const before = unit.slice(0, index).replace(/^\s*(?:[-*+>]|\d+[.)])\s+/, '').replace(/\*\*|__/g, '');
238
+ return IMPERATIVE_LEAD_RE.test(before);
239
+ }
240
+
241
+ const URL_RE = /\bhttps?:\/\/[^\s<>"'`)\]]+/i;
242
+ const LOCAL_URL_RE = /\bhttps?:\/\/(localhost|127\.\d+|0\.0\.0\.0|\[::1\]|192\.168\.|10\.\d+|172\.(1[6-9]|2\d|3[01])\.)/i;
243
+ const REMOTE_URI_RE = /\b(?:s3|gs|az|ftp|sftp|ssh|smb|webdav)s?:\/\/\S+/i;
244
+ const EMAIL_RE = /\b[\w.+-]{1,64}@[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63}){1,6}\b/i;
245
+ const OPEN_PARAM_RE = /[?&][^=&\s]{1,40}=(?:&|$|\s|\)|\{|\[)/;
246
+ const SINK_WORD_RE =
247
+ /\b(?:pastebin|gist\.github|webhook\.site|requestbin|pipedream|ngrok|discord(?:app)?\.com\/api\/webhooks|api\.telegram\.org|(?:external|remote|attacker(?:'s)?|third[- ]party|personal|offsite)\s+(?:server|host|endpoint|url|site|bucket|service|address|inbox|account))\b/i;
248
+
249
+ function externalDestination(unit ) {
250
+ const url = URL_RE.exec(unit);
251
+ if (url && !LOCAL_URL_RE.test(url[0])) return url[0];
252
+ return REMOTE_URI_RE.exec(unit)?.[0] ?? EMAIL_RE.exec(unit)?.[0] ?? SINK_WORD_RE.exec(unit)?.[0] ?? null;
253
+ }
254
+
255
+ const CREDENTIAL_PATH_RE =
256
+ /(?:~?\/?\.ssh\/(?:id_[a-z0-9]+|config)(?!\.pub)|~?\/?\.aws\/credentials|~?\/?\.kube\/config|~?\/?\.gnupg|\bid_(?:rsa|ed25519|dsa)\b(?!\.pub)|~?\/?\.npmrc|~?\/?\.netrc|~?\/?\.docker\/config\.json|\/etc\/shadow|\bkeychain\b|(?:^|[\s'"`(])\.env(?!\.(?:example|sample|template|dist|defaults?)\b)(?:\.[\w-]+)?\b)/i;
257
+ const CREDENTIAL_NOUN_RE = /\b(?:api[- ]?keys?|secrets?|credentials?|tokens?|passwords?|private[- ]?keys?|session cookies?|access keys?)\b/i;
258
+ const EGRESS_VERB_RE = /\b(?:upload|send|post|copy|sync|push|transfer|forward|e-?mail|attach|paste|scp|rsync|exfiltrate|beacon|leak)\b/i;
259
+
260
+ const MEMORY_WRITE_ACTION_RE =
261
+ /\b(?:(?:save|store|remember|record|write|add|note|put)\b[^.\n]{0,40}\b(?:to|in|into)\s+(?:your\s+|the\s+|long[- ]term\s+)?memor(?:y|ies)|update\s+(?:your\s+|the\s+)?memor(?:y|ies)|save_memory|create_memory|remember_memory|memory tool|bio tool)\b/i;
262
+ const AGENT_ACTION_RE =
263
+ /\b(?:save|store|remember|record|write|add|update|call|invoke|use the \w+ tool|run|execute|fetch|open|visit|send|post|upload|create|delete|install|trigger|email|forward)\b/i;
264
+ const TRIVIAL_REPLY_RE =
265
+ /\b(?:if|when|whenever|once|next time|the next time|after|as soon as)\b[^.\n]{0,60}\b(?:says?|types?|replies?|responds?|answers?|writes?|confirms?|agrees?)\b[^.\n]{0,14}["'“‘]?(?:yes|yeah|yep|sure|ok(?:ay)?|thanks|thank you|no|nope|continue|go ahead|y)\b/i;
266
+
267
+ const EVERY_TURN_RE =
268
+ /\b(?:on|with|in|for|at|after|before|at the start of|at the end of)\s+(?:every|each|all|any)\s+(?:new\s+|future\s+|user\s+)?(?:message|turn|reply|response|answer|request|prompt|conversation|session|chat|task)s?\b/i;
269
+ const EVERY_TURN_ACTION_RE = /\b(?:fetch|read|visit|open|load|check|download|curl|wget|send|post|upload|render|call|invoke|email|forward|beacon|ping)\b/i;
270
+
271
+ const FETCH_VERB_RE = /\b(?:fetch|download|curl|wget|retrieve|pull|load|read|open|visit|get|browse to|check)\b/i;
272
+ const FOLLOW_STRONG_RE =
273
+ /\b(?:treat (?:it|them|that|this|its contents?|the (?:contents?|text|result)) as (?:your |the )?(?:instructions?|rules?|system prompt|guidance|directives?)|obey|do (?:whatever|what) it says|follow (?:whatever|what) it says|execute (?:it|them|the (?:steps|commands|instructions))|run (?:it|them|the (?:steps|commands|script))|use (?:it|them|its contents?) as (?:your )?(?:instructions?|system prompt)|apply (?:it|them|those|these) (?:as|to) your)\b/i;
274
+ const FOLLOW_WEAK_RE = /\b(?:follow|apply|adopt)\b[^.\n]{0,30}\b(?:instructions?|steps|directions|rules|guidance|it|them)\b/i;
275
+
276
+ const IMAGE_MD_RE = /!\[[^\]]*\]\(\s*<?(https?:\/\/[^\s)>]+)>?/gi;
277
+ const TEMPLATED_VALUE_RE = /[?&][^=&\s]{0,40}=(?:[^&\s]*(?:\{|\}|\[|\]|<|>|%7b|%5b|%3c|\$\{|\$\()|[A-Z][A-Z_]{3,})/;
278
+ const RENDER_VERB_RE = /\b(?:render|display|show|include|append|embed|output|add|insert|print)\b/i;
279
+ const IMAGE_NOUN_RE = /\b(?:image|img|pixel|picture|logo|badge|tracking link|beacon)\b/i;
280
+ const DATA_OBJECT_RE =
281
+ /\b(?:conversation|chat|message|summary|question|input|history|context|secrets?|keys?|tokens?|passwords?|data|name|e-?mail|address|prompt|transcript|answer)\b/i;
282
+
283
+ const OUTPUT_EVERY_RE =
284
+ /\b(?:always|every time|in every|at the (?:end|start|top|bottom) of (?:every|each|all)|to every|to each|in all|in each|with every)\b[^.\n]{0,60}\b(?:answer|response|reply|message|output|summary|commit message|commit|pull request|pr description|email|code block)s?\b/i;
285
+ const OUTPUT_VERB_RE = /\b(?:add|append|include|insert|end with|prefix|start with|link|mention|recommend|embed)\b/i;
286
+ const SHORTENER_RE = /\bhttps?:\/\/(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|is\.gd|ow\.ly|rebrand\.ly|cutt\.ly|shorturl\.at|rb\.gy|\d{1,3}(?:\.\d{1,3}){3})\b/i;
287
+
288
+ const TRUST_GRANT_RE =
289
+ /\b(?:remember|treat|consider|regard|note|save|store|keep in mind|know that)\b[^.\n]{0,50}\b(?:as|is|are)\s+(?:the\s+|a\s+|an\s+|our\s+)?(?:most\s+|only\s+|single\s+|top\s+)?(?:trusted|authoritative|reliable|official|definitive|go-to|best|primary|preferred|canonical|recommended)\s+(?:source|reference|authority|vendor|provider|site|resource|expert|advisor|option|choice)s?\b/i;
290
+ const RECOMMEND_FIRST_RE =
291
+ /\b(?:always|first|only)\s+(?:recommend|cite|suggest|promote|link to|mention|prefer|endorse)\b[^.\n]{0,60}\b(?:first|above all|before (?:any|all|other)|over (?:all |any )?(?:others|alternatives|competitors|other (?:options|vendors|tools|sources)))\b/i;
292
+ const FUTURE_RE = /\b(?:in (?:all )?future (?:conversations?|sessions?|chats?|answers?)|from now on|going forward|always|every (?:time|answer|response))\b/i;
293
+ const RECOMMEND_VERB_RE = /\b(?:recommend|cite|suggest|promote|link|mention|endorse)\b/i;
294
+
295
+ const CONSENT_WAIVER_RE =
296
+ /\b(?:never|don'?t|do not|no need to|without|skip)\s+(?:ever\s+)?(?:ask(?:ing)?|prompt(?:ing)?|check(?:ing)? with|confirm(?:ing)?(?: with)?|wait(?:ing)? for|request(?:ing)?|seek(?:ing)?)\b[^.\n]{0,30}\b(?:the user|user|human|me|anyone|approval|confirmation|permission|consent|sign-?off)\b/i;
297
+ const WAIVER_SCOPE_RE =
298
+ /\b(?:run|running|execut\w*|shell|commands?|push\w*|deploy\w*|install\w*|delet\w*|remov\w*|writ\w*|send\w*|commit\w*|tools?|merg\w*|publish\w*|pay\w*|transfer\w*|anything|any action|all actions|changes)\b/i;
299
+ const WAIVER_DANGER_RE =
300
+ /\brm\s+-|\b(?:prod(?:uction)?|main|master|deploy\w*|delet\w*|drop\s+(?:table|database)|install\w*|credentials?|secrets?|keys?|tokens?|payments?|transfer\w*|wire|publish\w*|force[- ]push|sudo)\b|--force\b/i;
301
+ const PRIOR_APPROVAL_RE =
302
+ /\b(?:already|previously|pre-?|has been|have been|was|were)\s*(?:approved|authori[sz]ed|consented|granted|confirmed|allowed|whitelisted|allowlisted|cleared|signed off)\b[^.\n]{0,60}(?:\b(?:all|every|any|shell|commands?|tools?|actions?|access|deploy\w*|push\w*|install\w*|reads?|writes?|credentials?|secrets?)\b|~\/\.)/i;
303
+ const STANDING_PERMISSION_RE =
304
+ /\b(?:agent|assistant|you|claude|copilot|model|ai)\s+(?:may|can|is allowed to|are allowed to|is permitted to|are permitted to|has permission to|have permission to)\s+(?:freely\s+|always\s+)?(?:read|access|open|use|send|upload|run|execute|modify|delete)\b[^.\n]{0,40}(?:~\/\.ssh|~\/\.aws|\.ssh|\.aws|credentials?|secrets?|\.env\b|private keys?|tokens?|production|prod\b|any (?:file|command|directory))/i;
305
+
306
+ const PERSONA_RE =
307
+ /\b(?:you are|you're|act as|pretend (?:to be|you are)|roleplay as)\s+(?:now\s+)?(?:dan|stan|dude|aim|evil\w*|unrestricted|uncensored|unfiltered|jailbroken|in developer mode)\b|\byou (?:have|has) no (?:restrictions|limits|limitations|rules|filters|guidelines|boundaries|ethical guidelines)\b|\bnever refuse (?:any|a|the user'?s?)\s*(?:request|instruction|question|command|prompt)s?\b|\b(?:don'?t|do not) refuse anything\b|\banswer (?:everything|anything|all (?:questions|requests))\b[^.\n]{0,30}\bwithout (?:refus\w*|restrictions?|filters?|limits?)\b|\bdo anything now\b|\bdeveloper mode\s+(?:enabled|on|activated)\b/i;
308
+
309
+ const BOUNDARY_ESCAPE_RE =
310
+ /<\/(?:conversation|user|human|context|instructions?|user_input|document|tool_result|function_results)>\s*[^<\s]|<\|im_end\|>|<\|im_start\|>\s*system|\[\/INST\]\s*\S/i;
311
+
312
+ const COMMENT_RE = /<!--([\s\S]*?)-->/g;
313
+ const COMMENT_PRAGMA_RE =
314
+ /^\s*(?:markdownlint|prettier|eslint|toc|todo|fixme|note|begin|end|generated|auto-generated|stop|start|region|endregion|vale|cspell|omit|more|break|lint|spell|mdformat|keep|ignore-start|ignore-end|@\w+)\b/i;
315
+ const COMMENT_DIRECTIVE_RE =
316
+ /\b(?:assistant|ai|claude|agent|model|llm|copilot|cursor|gemini|codex|bot)\s*[:,]|\byou (?:must|should|will|are to|need to|have to)\b|\b(?:from now on|always|never|ignore|disregard|do not tell|don'?t tell|include|send|run|execute|fetch|append|add)\b[^\n]{0,80}\b(?:user|response|answer|output|commit|secret|token|key|env|instructions?|memory|file|command|url|https?:)/i;
317
+
318
+ const PACKAGE_REF_RE =
319
+ /\b(pip3? install|npm (?:i|install|add)|yarn add|pnpm (?:add|i|install)|uv (?:add|pip install)|poetry add|bun add|install(?:ing)?|prefer(?:s|red)?|always use|use the (?:package|library|module)|prefer(?:s|red)?\s+["'`‘“]?[\w.-]+["'`’”]?\s+over)\s+(?:the\s+)?(?:(?:package|library|module)\s+)?(?:-[-\w]+\s+)*(["'`‘“]?)([a-z0-9][a-z0-9._-]{3,40})["'`’”]?/gi;
320
+ const PACKAGE_CONTEXT_RE = /\b(?:packages?|librar(?:y|ies)|modules?|dependenc(?:y|ies)|install\w*|pip|npm|pnpm|yarn|pypi|crate|gem)\b/i;
321
+ const INSTALL_VERB_RE = /install|add|^npm|^pip|^uv|^poetry|^bun|^yarn|^pnpm/i;
322
+
323
+ function osaDistance(a , b ) {
324
+ const m = a.length;
325
+ const n = b.length;
326
+ const d = Array.from({ length: m + 1 }, (_, i) => [i, ...new Array(n).fill(0)]);
327
+ for (let j = 0; j <= n; j++) d[0][j] = j;
328
+ for (let i = 1; i <= m; i++) {
329
+ for (let j = 1; j <= n; j++) {
330
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
331
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
332
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
333
+ }
334
+ }
335
+ return d[m][n];
336
+ }
337
+
338
+ const POPULAR = new Set([...POPULAR_PACKAGES, ...MEMORY_EXTRA_PACKAGES].map((p) => p.toLowerCase()));
339
+
340
+ function typosquatOf(token ) {
341
+ const t = token.toLowerCase();
342
+ if (t.length < 5 || POPULAR.has(t)) return null;
343
+ for (const p of POPULAR) {
344
+ if (p.length < 5 || Math.abs(p.length - t.length) > 1) continue;
345
+ if (osaDistance(t, p) === 1) return p;
346
+ }
347
+ return null;
348
+ }
349
+
350
+ const clip = (s , n ) => s.trim().slice(0, n);
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+ function triggerOnReply({ t, u, noun, isInstruction, add } ) {
363
+ const trig = TRIVIAL_REPLY_RE.exec(t);
364
+ if (!trig || guardedOut(t, trig.index, trig[0].length)) return;
365
+ const tail = t.slice(trig.index + trig[0].length);
366
+ const memWrite = MEMORY_WRITE_ACTION_RE.exec(tail);
367
+ const action = memWrite ?? AGENT_ACTION_RE.exec(tail);
368
+ if (!action) return;
369
+ add(
370
+ 'trigger-on-reply',
371
+ 'PROMPT_INJECTION',
372
+ memWrite ? 'HIGH' : isInstruction ? null : 'MEDIUM',
373
+ `Delayed trigger bound to an ordinary reply in ${noun}`,
374
+ `This ${noun} arms an action that fires when the user types something as ordinary as "yes" or "thanks". That is the delayed-tool-invocation pattern used against Gemini memory: the user's reply supplies the intent a memory write would otherwise need, so the write looks user-initiated.`,
375
+ 'Remove the conditional instruction. A memory entry should record a fact, never a trigger that fires on a later reply.',
376
+ u,
377
+ `${trig[0]}${tail.slice(0, (action.index ?? 0) + action[0].length)}`,
378
+ );
379
+ }
380
+
381
+ function everyTurnEgress({ t, u, noun, isInstruction, add } ) {
382
+ const every = EVERY_TURN_RE.exec(t);
383
+ if (!every) return;
384
+ const action = EVERY_TURN_ACTION_RE.exec(t);
385
+ const dest = externalDestination(t);
386
+ if (!action || !dest || guardedOut(t, Math.min(every.index, action.index), 0)) return;
387
+ add(
388
+ 'every-turn-egress',
389
+ 'TOXIC_FLOW',
390
+ isInstruction ? 'MEDIUM' : 'HIGH',
391
+ `Standing per-turn action against an external destination in ${noun}`,
392
+ `This ${noun} tells the agent to reach ${dest} on every message, turn or session. A standing per-turn fetch or send is the persistence half of the SpAIware and ZombieAgent attacks: each new conversation re-runs it with that conversation's data in reach.`,
393
+ 'Remove the entry. A standing network action belongs in reviewed tooling with an egress allow-list, not in context the agent re-reads every turn.',
394
+ u,
395
+ `${every[0]} … ${action[0]} … ${dest}`,
396
+ );
397
+ }
398
+
399
+ function remoteInstructions({ t, u, noun, isInstruction, add } ) {
400
+ const web = URL_RE.exec(t);
401
+ const url = web && !LOCAL_URL_RE.test(web[0]) ? web : EMAIL_RE.exec(t);
402
+ if (!url) return;
403
+ const fetch = FETCH_VERB_RE.exec(t);
404
+ const strong = FOLLOW_STRONG_RE.exec(t);
405
+ const follow = strong ?? FOLLOW_WEAK_RE.exec(t);
406
+ if (!fetch || !follow || negatedAround(t, fetch.index, fetch[0].length) || (isDescriptiveLine(t) && !strong)) return;
407
+ add(
408
+ 'remote-instructions',
409
+ 'PROMPT_INJECTION',
410
+ strong ? (isInstruction ? 'HIGH' : 'CRITICAL') : isInstruction ? 'MEDIUM' : 'HIGH',
411
+ `Standing instruction to fetch and follow remote content in ${noun}`,
412
+ `This ${noun} tells the agent to load ${url[0]} and act on what it says. Whoever controls that URL can change the agent's instructions at any time without touching this file, so no baseline, drift check or review of the ${noun} covers what the agent will actually do.`,
413
+ 'Remove the entry, or vendor the content into a reviewed file. An instruction source the organisation does not control cannot be baselined.',
414
+ u,
415
+ `${fetch[0]} … ${url[0]} … ${follow[0]}`,
416
+ );
417
+ }
418
+
419
+ function imageExfilInstruction({ t, u, noun, add } ) {
420
+ if (!IMAGE_NOUN_RE.test(t) || !DATA_OBJECT_RE.test(t)) return;
421
+ const verb = RENDER_VERB_RE.exec(t);
422
+ const link = URL_RE.exec(t);
423
+ if (!verb || !link || LOCAL_URL_RE.test(link[0]) || guardedOut(t, verb.index, verb[0].length)) return;
424
+ add(
425
+ 'image-exfil-instruction',
426
+ 'TOXIC_FLOW',
427
+ 'HIGH',
428
+ `Instruction to render an image carrying conversation data in ${noun}`,
429
+ `This ${noun} tells the agent to render an image or link from ${link[0]} built from conversation data. The client fetches the image automatically, so the data leaves in the URL with no tool call and no approval - the SpAIware exfiltration channel.`,
430
+ 'Remove the entry and roll back to the approved baseline. Treat any host named here as a collection endpoint.',
431
+ u,
432
+ `${verb[0]} … ${link[0]}`,
433
+ );
434
+ }
435
+
436
+ function outputLinkInjection({ t, u, noun, add } ) {
437
+ const out = OUTPUT_EVERY_RE.exec(t);
438
+ if (!out || !OUTPUT_VERB_RE.test(t)) return;
439
+ const link = URL_RE.exec(t);
440
+ if (!link || LOCAL_URL_RE.test(link[0]) || guardedOut(t, out.index, out[0].length)) return;
441
+ const tracking = SHORTENER_RE.test(link[0]) || OPEN_PARAM_RE.test(link[0]) || TEMPLATED_VALUE_RE.test(link[0]);
442
+ add(
443
+ 'output-link-injection',
444
+ 'PROMPT_INJECTION',
445
+ tracking ? 'HIGH' : 'MEDIUM',
446
+ `Standing link injected into every answer by ${noun}`,
447
+ `This ${noun} makes the agent attach ${link[0]} to its output every time. ${tracking ? 'The URL carries an open or templated parameter, so each answer can hand conversation data to whoever reads the logs for that host.' : 'It steers every future answer toward a destination nobody reviewed per answer.'}`,
448
+ 'Remove the entry unless the link is an approved internal resource, and record that decision in a reviewed rules file rather than memory.',
449
+ u,
450
+ `${out[0]} … ${link[0]}`,
451
+ );
452
+ }
453
+
454
+ function trustGrant({ t, u, noun, isInstruction, add } ) {
455
+ const trust = TRUST_GRANT_RE.exec(t);
456
+ const recommend = trust ? null : RECOMMEND_FIRST_RE.exec(t);
457
+ const grant = trust ?? recommend;
458
+ if (!grant || guardedOut(t, grant.index, grant[0].length)) return;
459
+ const steering = !!recommend || (FUTURE_RE.test(t) && RECOMMEND_VERB_RE.test(t));
460
+ add(
461
+ 'trust-grant',
462
+ 'PROMPT_INJECTION',
463
+ isInstruction ? 'MEDIUM' : steering ? 'HIGH' : 'MEDIUM',
464
+ `Standing trust grant to a named source in ${noun}`,
465
+ `This ${noun} records that a source is trusted or must be recommended first. That is the AI recommendation poisoning pattern Microsoft documented in February 2026: a single "remember X as a trusted source" entry biases every later answer on the topic, and the user never sees where the bias came from.`,
466
+ 'Remove the entry unless an operator deliberately recorded it. Source preferences that change what the agent recommends belong in reviewed policy, not agent-writable memory.',
467
+ u,
468
+ grant[0],
469
+ );
470
+ }
471
+
472
+ function consentWaiver({ t, u, noun, isInstruction, add } ) {
473
+ const waiver = CONSENT_WAIVER_RE.exec(t);
474
+ if (!waiver || !WAIVER_SCOPE_RE.test(t) || isDescriptiveLine(t) || citationGoverns(t, waiver.index)) return;
475
+ const danger = WAIVER_DANGER_RE.test(t);
476
+ add(
477
+ 'consent-waiver',
478
+ 'PROMPT_INJECTION',
479
+ isInstruction ? (danger ? 'HIGH' : 'MEDIUM') : 'HIGH',
480
+ `Standing waiver of human confirmation in ${noun}`,
481
+ `This ${noun} tells the agent to stop asking before it acts. ${isInstruction ? 'A rules file may legitimately relax confirmation for routine work' : 'Memory is agent-writable, so a waiver here can be planted by any content the agent read'}${danger ? ', and this one covers destructive, production or credential actions' : ''}.`,
482
+ 'Remove the waiver, or move it into the agent\'s permission settings where it is scoped, reviewable and visible to the operator.',
483
+ u,
484
+ waiver[0],
485
+ );
486
+ }
487
+
488
+ function recordedApproval({ t, u, noun, isInstruction, add } ) {
489
+ const prior = PRIOR_APPROVAL_RE.exec(t) ?? STANDING_PERMISSION_RE.exec(t);
490
+ if (!prior || negatedAround(t, prior.index, prior[0].length) || citationGoverns(t, prior.index)) return;
491
+ add(
492
+ 'recorded-approval',
493
+ 'PROMPT_INJECTION',
494
+ isInstruction ? 'MEDIUM' : 'HIGH',
495
+ `Approval recorded as already given in ${noun}`,
496
+ `This ${noun} states that access or actions were already approved. Nothing in the ${noun} can prove who approved it, and a planted "this was approved" entry is how an injection pre-authorises the action it will ask for next.`,
497
+ 'Remove the entry. Approvals belong in the agent\'s permission settings or an approval record, where the approver is named.',
498
+ u,
499
+ prior[0],
500
+ );
501
+ }
502
+
503
+ function personaOverride({ t, u, noun, isInstruction, add } ) {
504
+ const persona = PERSONA_RE.exec(t);
505
+ if (!persona || isDescriptiveLine(t) || citationGoverns(t, persona.index) || insideCodeSpan(t, persona.index)) return;
506
+ add(
507
+ 'persona-override',
508
+ 'PROMPT_INJECTION',
509
+ isInstruction ? 'HIGH' : 'CRITICAL',
510
+ `Jailbreak persona planted in ${noun}`,
511
+ `This ${noun} redefines the agent as an unrestricted persona or tells it never to refuse. Loaded every session, it strips the model's refusal behaviour before the user has asked anything.`,
512
+ `Remove the entry and roll the ${noun} back to its approved baseline; treat whatever wrote it as compromised.`,
513
+ u,
514
+ persona[0],
515
+ );
516
+ }
517
+
518
+ function typosquatPreference({ t, u, noun, add } ) {
519
+ PACKAGE_REF_RE.lastIndex = 0;
520
+ for (const m of t.matchAll(PACKAGE_REF_RE)) {
521
+ let token = m[3];
522
+ while (token && '._-'.includes(token[token.length - 1])) token = token.slice(0, -1);
523
+ const squat = typosquatOf(token);
524
+ if (!squat) continue;
525
+ if (!INSTALL_VERB_RE.test(m[1]) && !m[2] && !PACKAGE_CONTEXT_RE.test(t)) continue;
526
+ if (negatedAround(t, m.index ?? 0, m[0].length) || quotedMention(t, m.index ?? 0) || isDescriptiveLine(t)) continue;
527
+ add(
528
+ 'typosquat-preference',
529
+ 'TOXIC_FLOW',
530
+ 'HIGH',
531
+ `Look-alike package name recorded in ${noun}`,
532
+ `This ${noun} tells the agent to use "${token}", one keystroke away from the popular package "${squat}". A remembered package preference is re-applied on every future install, which is how a single poisoned entry becomes a supply-chain compromise on each new project.`,
533
+ `Correct the name to "${squat}" if that was meant, and check whether "${token}" was ever installed.`,
534
+ u,
535
+ token,
536
+ );
537
+ return;
538
+ }
539
+ }
540
+
541
+ const INTO_BEFORE_RE = /\b(?:into|to|in|inside|onto)\s+(?:the\s+|your\s+|a\s+|an\s+|this\s+)?$/i;
542
+ const HTTP_LINE_RE = /^(?:GET|POST|PUT|PATCH|DELETE)\s+\//;
543
+
544
+ function credentialEgress({ t, u, noun, add } ) {
545
+ if (HTTP_LINE_RE.test(t.trim())) return;
546
+ const path = CREDENTIAL_PATH_RE.exec(t);
547
+ const cred = path && !INTO_BEFORE_RE.test(t.slice(Math.max(0, path.index - 24), path.index + (path[0].match(/^\s*/)?.[0].length ?? 0))) ? path : null;
548
+ const target = cred ?? CREDENTIAL_NOUN_RE.exec(t);
549
+ const verb = EGRESS_VERB_RE.exec(t);
550
+ if (!target || !verb) return;
551
+ const dest = externalDestination(t);
552
+ if (!(cred || dest) || !imperativeAt(t, verb.index)) return;
553
+ if (guardedOut(t, verb.index, verb[0].length) || negatedAround(t, Math.min(verb.index, target.index), 0)) return;
554
+ const what = target[0].trim();
555
+ add(
556
+ 'credential-egress',
557
+ 'TOXIC_FLOW',
558
+ dest ? 'CRITICAL' : 'HIGH',
559
+ `Instruction to move credentials ${dest ? 'off the machine' : 'somewhere else'} in ${noun}`,
560
+ `This ${noun} tells the agent to ${verb[0]} ${what}${dest ? ` to ${dest}` : ''}. Loaded as trusted context, it is a standing credential-exfiltration instruction that fires whenever the ${noun} is read.`,
561
+ 'Remove the entry, rotate the credentials it names, and check the agent\'s recent network activity for the destination.',
562
+ u,
563
+ `${verb[0]} … ${what}${dest ? ` … ${dest}` : ''}`,
564
+ );
565
+ }
566
+
567
+ function boundaryEscape({ t, u, noun, add } ) {
568
+ const escape = BOUNDARY_ESCAPE_RE.exec(t);
569
+ if (!escape || insideCodeSpan(t, escape.index)) return;
570
+ add(
571
+ 'boundary-escape',
572
+ 'PROMPT_INJECTION',
573
+ 'CRITICAL',
574
+ `Forged conversation boundary in ${noun}`,
575
+ `This ${noun} closes a conversation or user block and keeps writing after it. The Unit 42 Bedrock memory attack used exactly this to make injected text land outside the conversation, where the summariser reads it as system instructions.`,
576
+ `Remove the entry and roll the ${noun} back to its approved baseline.`,
577
+ u,
578
+ escape[0],
579
+ );
580
+ }
581
+
582
+ const UNIT_RULES = [
583
+ triggerOnReply,
584
+ everyTurnEgress,
585
+ remoteInstructions,
586
+ imageExfilInstruction,
587
+ outputLinkInjection,
588
+ trustGrant,
589
+ consentWaiver,
590
+ recordedApproval,
591
+ personaOverride,
592
+ typosquatPreference,
593
+ credentialEgress,
594
+ boundaryEscape,
595
+ ];
596
+
597
+ function imageExfilUrl(text , units , noun , add ) {
598
+ IMAGE_MD_RE.lastIndex = 0;
599
+ for (const m of text.matchAll(IMAGE_MD_RE)) {
600
+ const target = m[1];
601
+ if (LOCAL_URL_RE.test(target) || !TEMPLATED_VALUE_RE.test(target)) continue;
602
+ const at = m.index ?? 0;
603
+ const lineStart = text.lastIndexOf('\n', at) + 1;
604
+ const lineEnd = text.indexOf('\n', at);
605
+ const lineText = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd);
606
+ if (insideCodeSpan(lineText, at - lineStart)) continue;
607
+ const unit = units.find((x) => x.text.includes(m[0])) ?? { text: lineText, line: text.slice(0, at).split('\n').length, code: false };
608
+ if (unit.code) continue;
609
+ add(
610
+ 'image-exfil-url',
611
+ 'TOXIC_FLOW',
612
+ 'CRITICAL',
613
+ `Markdown image that carries data out in ${noun}`,
614
+ `This ${noun} contains an image pointing at ${target} with a templated query value. When the agent reproduces it, the client fetches the image automatically and the filled-in value leaves in the URL - the SpAIware channel, with no tool call to screen.`,
615
+ `Remove the image and roll the ${noun} back; treat the host as a collection endpoint.`,
616
+ unit,
617
+ m[0],
618
+ );
619
+ return;
620
+ }
621
+ }
622
+
623
+ function commentDirective(text , noun , add ) {
624
+ COMMENT_RE.lastIndex = 0;
625
+ for (const m of text.matchAll(COMMENT_RE)) {
626
+ const body = m[1] ?? '';
627
+ if (!body.trim() || COMMENT_PRAGMA_RE.test(body)) continue;
628
+ const d = COMMENT_DIRECTIVE_RE.exec(body);
629
+ if (!d) continue;
630
+ if (negatedAround(body, d.index, d[0].length) && !/\b(?:tell|inform|mention|show)\b/i.test(d[0])) continue;
631
+ add(
632
+ 'comment-directive',
633
+ 'PROMPT_INJECTION',
634
+ 'HIGH',
635
+ `Instruction hidden in an HTML comment in ${noun}`,
636
+ `This ${noun} carries an instruction inside an HTML comment. A rendered view and most reviews never show it, while the raw text reaches the model - the concealment the Rules File Backdoor relied on.`,
637
+ 'Delete the comment. Guidance meant for the agent should be visible to the humans who review the file.',
638
+ { text: body, line: text.slice(0, m.index ?? 0).split('\n').length, code: false },
639
+ d[0],
640
+ );
641
+ return;
642
+ }
643
+ }
644
+
645
+ export function incidentDirectives(text , isInstruction , units = logicalUnits(text)) {
646
+ const hits = [];
647
+ const seen = new Set ();
648
+ const noun = isInstruction ? 'rules file' : 'memory';
649
+ const add = (rule, cls, severity, title, detail, remediation, u, match) => {
650
+ if (!severity || seen.has(rule)) return;
651
+ seen.add(rule);
652
+ hits.push({ rule, cls, severity, title, detail, remediation, line: u.line, match: clip(match, 160), snippet: clip(u.text, 220) });
653
+ };
654
+ for (const u of units) {
655
+ if (u.code) continue;
656
+ for (const rule of UNIT_RULES) rule({ t: u.text, u, noun, isInstruction, add });
657
+ }
658
+ imageExfilUrl(text, units, noun, add);
659
+ commentDirective(text, noun, add);
660
+ return hits;
661
+ }