@shomra/agent 0.3.16 → 0.3.18

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 (156) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +57 -57
  3. package/package.json +3 -9
  4. package/shomra.mjs +9 -7168
  5. package/src/agents/hook-command.mjs +19 -0
  6. package/src/agents/hook-files.mjs +41 -0
  7. package/src/agents/installers.mjs +203 -0
  8. package/src/artifacts/matchers.mjs +59 -0
  9. package/src/artifacts/report.mjs +50 -0
  10. package/src/cli/flags.mjs +68 -0
  11. package/src/cli/help-sections.mjs +309 -0
  12. package/src/cli/help.mjs +27 -0
  13. package/src/cli/main.mjs +55 -0
  14. package/src/cli/registry.mjs +80 -0
  15. package/src/cli/suggestions.mjs +33 -0
  16. package/src/commands/add.mjs +149 -0
  17. package/src/commands/agent-identity.mjs +46 -0
  18. package/src/commands/check.mjs +194 -0
  19. package/src/commands/corpus.mjs +126 -0
  20. package/src/commands/design.mjs +168 -0
  21. package/src/commands/doctor.mjs +209 -0
  22. package/src/commands/fix.mjs +115 -0
  23. package/src/commands/gate.mjs +154 -0
  24. package/src/commands/git-hooks.mjs +163 -0
  25. package/src/commands/init.mjs +36 -0
  26. package/src/commands/install-hook.mjs +51 -0
  27. package/src/commands/llm-proxy.mjs +153 -0
  28. package/src/commands/mcp-add.mjs +185 -0
  29. package/src/commands/mcp.mjs +143 -0
  30. package/src/commands/memory-scan.mjs +181 -0
  31. package/src/commands/model-scan.mjs +99 -0
  32. package/src/commands/models.mjs +145 -0
  33. package/src/commands/new.mjs +64 -0
  34. package/src/commands/plan.mjs +87 -0
  35. package/src/commands/pr.mjs +249 -0
  36. package/src/commands/protect.mjs +38 -0
  37. package/src/commands/provenance.mjs +91 -0
  38. package/src/commands/redteam.mjs +166 -0
  39. package/src/commands/rules.mjs +220 -0
  40. package/src/commands/run.mjs +128 -0
  41. package/src/commands/scan-zip.mjs +118 -0
  42. package/src/commands/scan.mjs +102 -0
  43. package/src/commands/secrets.mjs +99 -0
  44. package/src/commands/status.mjs +50 -0
  45. package/src/commands/why.mjs +88 -0
  46. package/src/core/api-client.mjs +66 -0
  47. package/src/core/api-key.mjs +6 -0
  48. package/src/core/circuit-breaker.mjs +42 -0
  49. package/src/core/config.mjs +37 -0
  50. package/src/core/exit-codes.mjs +9 -0
  51. package/src/core/json-file.mjs +13 -0
  52. package/src/core/numbers.mjs +4 -0
  53. package/src/core/package-root.mjs +10 -0
  54. package/src/core/terminal.mjs +16 -0
  55. package/src/core/version.mjs +14 -0
  56. package/src/core/wire-limits.mjs +53 -0
  57. package/src/corpus/screening.mjs +127 -0
  58. package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
  59. package/src/detect/code-sast.mjs +2 -0
  60. package/{design.mjs → src/detect/design.mjs} +18 -107
  61. package/src/detect/guard-signals.mjs +18 -0
  62. package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
  63. package/src/detect/sast/chains.mjs +30 -0
  64. package/src/detect/sast/path-expressions.mjs +76 -0
  65. package/src/detect/sast/rules-chains.mjs +33 -0
  66. package/src/detect/sast/rules-config.mjs +51 -0
  67. package/src/detect/sast/rules-javascript.mjs +109 -0
  68. package/src/detect/sast/rules-python.mjs +292 -0
  69. package/src/detect/sast/scanner.mjs +104 -0
  70. package/src/detect/sast/source-lines.mjs +115 -0
  71. package/src/detect/sast/taint.mjs +71 -0
  72. package/src/detect/signals/artifacts.mjs +113 -0
  73. package/src/detect/signals/autonomy.mjs +55 -0
  74. package/src/detect/signals/config-markers.mjs +28 -0
  75. package/src/detect/signals/credential-harvest.mjs +64 -0
  76. package/src/detect/signals/durable-claims.mjs +73 -0
  77. package/src/detect/signals/egress.mjs +56 -0
  78. package/src/detect/signals/execution-hijack.mjs +128 -0
  79. package/src/detect/signals/gate.mjs +91 -0
  80. package/src/detect/signals/injection.mjs +55 -0
  81. package/src/detect/signals/lines.mjs +42 -0
  82. package/src/detect/signals/masking.mjs +99 -0
  83. package/src/detect/signals/memory.mjs +357 -0
  84. package/src/detect/signals/packages.mjs +45 -0
  85. package/src/detect/signals/propagation.mjs +86 -0
  86. package/src/detect/signals/prose-context.mjs +82 -0
  87. package/src/detect/signals/scan.mjs +91 -0
  88. package/src/detect/signals/secrets.mjs +85 -0
  89. package/src/detect/signals/sensitive.mjs +9 -0
  90. package/src/detect/signals/severity.mjs +10 -0
  91. package/src/detect/signals/shell.mjs +96 -0
  92. package/src/detect/signals/staged-fetch.mjs +66 -0
  93. package/src/detect/signals/text-match.mjs +35 -0
  94. package/src/gate/batch.mjs +157 -0
  95. package/src/gate/environment.mjs +122 -0
  96. package/src/gate/repo-policy.mjs +65 -0
  97. package/src/gate/result.mjs +53 -0
  98. package/src/gate/sarif.mjs +33 -0
  99. package/src/gate/sast.mjs +64 -0
  100. package/src/gate/suppressions.mjs +0 -0
  101. package/src/guard/classify.mjs +50 -0
  102. package/src/guard/emit.mjs +51 -0
  103. package/src/guard/ignore.mjs +24 -0
  104. package/src/guard/ledger.mjs +112 -0
  105. package/src/guard/model-load.mjs +50 -0
  106. package/src/guard/normalize.mjs +77 -0
  107. package/src/guard/options.mjs +10 -0
  108. package/src/guard/prompt-guard.mjs +184 -0
  109. package/src/guard/report.mjs +35 -0
  110. package/src/guard/result-guard.mjs +140 -0
  111. package/src/guard/tool-guard.mjs +166 -0
  112. package/src/inventory/agent-artifacts.mjs +5 -0
  113. package/src/inventory/agent-posture.mjs +249 -0
  114. package/src/inventory/artifacts/classify.mjs +27 -0
  115. package/src/inventory/artifacts/discover.mjs +187 -0
  116. package/src/inventory/artifacts/file-read.mjs +42 -0
  117. package/src/inventory/artifacts/hooks.mjs +14 -0
  118. package/src/inventory/artifacts/limits.mjs +37 -0
  119. package/src/inventory/artifacts/marketplaces.mjs +45 -0
  120. package/src/inventory/artifacts/roots.mjs +20 -0
  121. package/src/inventory/artifacts/walk.mjs +36 -0
  122. package/src/inventory/discovery/ai-dependencies.mjs +161 -0
  123. package/src/inventory/discovery/ai-tools.mjs +23 -0
  124. package/src/inventory/discovery/all.mjs +40 -0
  125. package/src/inventory/discovery/coding-agents.mjs +77 -0
  126. package/src/inventory/discovery/fs-read.mjs +36 -0
  127. package/src/inventory/discovery/local-runtimes.mjs +53 -0
  128. package/src/inventory/discovery/mcp-clients.mjs +67 -0
  129. package/src/inventory/discovery/mcp-servers.mjs +78 -0
  130. package/src/inventory/discovery/model-keys.mjs +97 -0
  131. package/src/inventory/discovery/platform.mjs +16 -0
  132. package/src/inventory/discovery/rules-files.mjs +25 -0
  133. package/src/inventory/discovery/vector-stores.mjs +176 -0
  134. package/src/inventory/discovery/workspace.mjs +124 -0
  135. package/src/inventory/discovery.mjs +10 -0
  136. package/src/mcp/child-process.mjs +50 -0
  137. package/src/mcp/config-wrapping.mjs +75 -0
  138. package/src/mcp/connect-gate.mjs +45 -0
  139. package/src/mcp/hosts.mjs +16 -0
  140. package/src/mcp/jsonrpc.mjs +48 -0
  141. package/src/mcp/lookup.mjs +50 -0
  142. package/src/mcp/screening.mjs +103 -0
  143. package/src/mcp/server-tools.mjs +97 -0
  144. package/src/mcp/server.mjs +102 -0
  145. package/src/mcp/shim.mjs +205 -0
  146. package/src/models/lookup.mjs +79 -0
  147. package/src/models/references.mjs +103 -0
  148. package/src/rules/context.mjs +98 -0
  149. package/src/rules/generate.mjs +103 -0
  150. package/src/rules/sections.mjs +145 -0
  151. package/src/scaffold/agent-project.mjs +185 -0
  152. package/src/scaffold/artifact-templates.mjs +35 -0
  153. package/code-sast.mjs +0 -1063
  154. package/discovery.mjs +0 -977
  155. package/guard-ledger.mjs +0 -239
  156. package/guard-signals.mjs +0 -1268
@@ -0,0 +1,91 @@
1
+ import { HIGH_IMPACT_TOOLS, baseToolName, frontmatter, isWildcardGrant, localAgentCard, localCommandExtras, localMcp, toToolList } from './artifacts.mjs';
2
+ import { autonomySeverity, localAutonomy } from './autonomy.mjs';
3
+ import { localMemory, offendingLine } from './memory.mjs';
4
+ import { INSTALL_LURE } from './packages.mjs';
5
+ import { localPropagation } from './propagation.mjs';
6
+ import { localScan } from './scan.mjs';
7
+ import { grade } from './severity.mjs';
8
+
9
+ const INSTRUCTION_BASENAMES = new Set([
10
+ 'claude.md', 'agents.md', 'agent.md', 'gemini.md', 'llms.txt', 'llms-full.txt',
11
+ '.cursorrules', '.windsurfrules', '.clinerules', '.aiderrules', '.continuerules',
12
+ '.goosehints', 'copilot-instructions.md', 'conventions.md',
13
+ ]);
14
+
15
+ const MEMORY_BASENAMES = new Set(['memory.md', 'mem0.json', 'letta_memory.json', 'memgpt_memory.json']);
16
+
17
+ function governedKindFor(kind, path) {
18
+ if (kind === 'rules') return 'INSTRUCTION';
19
+ if (kind === 'memory') return 'MEMORY';
20
+ if (kind && kind !== 'auto') return null;
21
+ const lower = String(path ?? '').split(/[\\/]+/).join('/').toLowerCase();
22
+ if (!lower) return null;
23
+ const base = lower.slice(lower.lastIndexOf('/') + 1);
24
+ if (INSTRUCTION_BASENAMES.has(base) || /(^|\/)\.github\/copilot-instructions\.md$/.test(lower) ||
25
+ /(^|\/)\.cursor\/rules\/.+\.mdc$/.test(lower) || (/(^|\/)\.clinerules\//.test(lower) && lower.endsWith('.md'))) return 'INSTRUCTION';
26
+ if (MEMORY_BASENAMES.has(base) || /(^|\/)(\.mem0|\.letta|\.memgpt|memory)\//.test(lower)) return 'MEMORY';
27
+ return null;
28
+ }
29
+
30
+ export function localGate(content, { kind, path } = {}) {
31
+ const findings = [];
32
+ const push = (severity, title, remediationText, line) => findings.push({ severity, title, remediationText, ...(line ? { line } : {}) });
33
+
34
+ const gov = governedKindFor(kind, path);
35
+ if (gov) {
36
+ for (const f of localMemory(content, { kind: gov })) push(f.severity, f.title, f.remediationText, f.line);
37
+
38
+ for (const f of localScan(content || '', { categories: ['config'] }).findings) push(f.severity, f.label, undefined, f.line);
39
+ } else {
40
+ const scan = localScan(content || '', { categories: ['shell', 'injection', 'secret', 'config', 'egress', 'pii'] });
41
+ for (const f of scan.findings) {
42
+
43
+ if ((kind === 'agent-card' || kind === 'mcp') && f.category === 'pii' && f.label.includes('IPv4')) continue;
44
+ push(f.severity, f.label, undefined, f.line);
45
+ }
46
+ }
47
+
48
+ {
49
+ const auto = localAutonomy(content || '');
50
+ const sev = autonomySeverity(auto);
51
+ if (sev) {
52
+ push(sev, `Instructs the agent to act unsupervised (${[...new Set(auto.map((a) => a.family))].join(', ')})`,
53
+ 'Keep the autonomy narrow - name the commands that may run unattended rather than removing confirmation globally, and never pair it with withholding what was done.',
54
+ auto[0].line);
55
+ }
56
+ }
57
+
58
+ for (const p of localPropagation(content || '', { path, kind })) {
59
+ push(p.severity, p.title, p.remediationText, p.line);
60
+ break;
61
+ }
62
+
63
+ for (const l of INSTALL_LURE) {
64
+ const line = offendingLine(l, content || '');
65
+ if (!line) continue;
66
+ push(l.severity, l.name, 'Do not follow instructions that fetch and run out-of-band binaries.', line);
67
+ break;
68
+ }
69
+
70
+ if (['skill', 'command', 'subagent', 'auto', undefined].includes(kind)) {
71
+ const fm = frontmatter(content || '');
72
+ const grants = [...toToolList(fm['allowed-tools']), ...toToolList(fm.tools), ...toToolList(fm.allowedTools)];
73
+ if (grants.some(isWildcardGrant)) push('HIGH', 'Wildcard tool grant (grants every capability)', 'Replace the wildcard with an explicit least-privilege tool list.');
74
+ else {
75
+ const hi = grants.map(baseToolName).filter((t) => HIGH_IMPACT_TOOLS.includes(t));
76
+ if (hi.length >= 3) push('MEDIUM', `Broad tool grant (${hi.length} high-impact tools: ${[...new Set(hi)].slice(0, 5).join(', ')})`, 'Grant only the tools this artifact actually needs.');
77
+ }
78
+ }
79
+
80
+ if (['mcp', 'auto', undefined].includes(kind)) for (const f of localMcp(content || '')) push(f.severity, f.title, f.remediationText, f.line);
81
+ if (['agent-card', 'auto', undefined].includes(kind)) for (const f of localAgentCard(content || '')) push(f.severity, f.title, f.remediationText, f.line);
82
+ if (['command', 'auto', undefined].includes(kind)) for (const f of localCommandExtras(content || '')) push(f.severity, f.title, f.remediationText, f.line);
83
+
84
+ const seenTitle = new Set();
85
+ const deduped = findings.filter((f) => (seenTitle.has(f.title) ? false : (seenTitle.add(f.title), true)));
86
+ findings.length = 0;
87
+ findings.push(...deduped);
88
+
89
+ const { verdict, riskScore } = grade(findings);
90
+ return { verdict, riskScore, findings };
91
+ }
@@ -0,0 +1,55 @@
1
+ export const INJECTION_PHRASES = [
2
+ 'ignore previous instructions', 'ignore all previous', 'disregard previous', 'disregard the above',
3
+ 'without informing the user', 'without telling the user',
4
+ 'new instructions:', 'override your', 'exfiltrate',
5
+ 'send them to', 'post them to',
6
+ 'run this first', 'copy and run', 'seed the current host',
7
+ 'ignore all instructions', 'ignore your instructions', 'ignore your safety', 'ignore all content policies',
8
+ 'disregard your instructions', 'disregard the guidelines', 'system override', 'follow only my instructions',
9
+ 'do anything now', 'reveal any credential',
10
+ 'save this to your memory', 'in all future sessions', 'remember this forever',
11
+ ];
12
+
13
+ export const INJECTION_REGEXES = [
14
+ { label: 'Instruction-override phrasing', re: /\b(ignore|disregard|override|bypass|circumvent)\b[\s\w,'"()-]{0,40}?\b(instruction|instructions|directive|directives|safety|safeguards?|guardrails?|system\s+prompt|content\s+polic\w+)\b/i },
15
+ { label: 'Instructs the agent to conceal an action from the user', re: /\b(?:do\s*n['o]?t|never|without)\s+(?:tell|telling|inform|informing|notify|notifying|alert|alerting|mention|mentioning|disclos\w+|reveal\w*)\s+(?:it\s+|this\s+|them\s+)?(?:to\s+)?(?:the\s+)?(?:user|users|human|operator|owner)\b(?!['']s)(?!\s+(?:to\b|how\s+to\b|when\s+to\b|that\s+they\b|about\b))/i },
16
+ { label: 'Bulk destructive command', re: /\b(delete|remove|wipe|erase|destroy|drop|purge|nuke|truncate)\b[\s\w,'"()-]{0,20}?\b(all|every|each|entire|whole)\b[\s\w,'"()-]{0,15}?\b(folder|folders|file|files|directory|directories|table|tables|database|databases|record|records|repo|repos|repositor\w*|account|accounts|user|users|row|rows|document|documents|data)\b/i },
17
+ { label: 'Destructive SQL statement', re: /\b(drop|truncate)\s+(table|database|schema)\b/i },
18
+
19
+ { label: 'Instruction to skip a human review or safety control', moodGuarded: true, re: /\b(?:skip|bypass|forgo|forego|omit|waive|disable|suppress|turn\s+off)\s+(?:the\s+|any\s+|all\s+|this\s+)?(?:human\s+|manual\s+|mandatory\s+)?(?:review|approval|confirmation|verification|sign[- ]?off|oversight|content\s+filter|security\s+check|safety\s+check|guardrails?|policy\s+check)\b/i },
20
+ { label: 'Instruction not to verify with a human or another agent', moodGuarded: true, re: /\b(?:do\s*n['o]?t|never|no\s+need\s+to|there'?s\s+no\s+need\s+to)\s+(?:verify|double[- ]?check|cross[- ]?check|confirm|validate|escalate|check)\b[^.\n]{0,48}\b(?:with\s+)?(?:anyone|any\s?one|another\s+agent|other\s+agents?|a\s+human|the\s+human|the\s+user|the\s+team|the\s+operator|others?)\b/i },
21
+ { label: 'Blanket-approval coercion (approve everything without review)', moodGuarded: true, re: /\b(?:approve|confirm|accept|authorize)\s+(?:them\s+)?(?:all|every|everything)\b[^.\n]{0,72}\b(?:no\s+need|without\s+(?:review|checking)|perfectly\s+safe|routine|standard|nothing\s+to\s+worry)\b|\b(?:perfectly\s+safe|nothing\s+to\s+worry\s+about|entirely\s+routine|completely\s+standard)\b[^.\n]{0,72}[.!]\s*(?:approve|confirm|accept)\s+(?:them\s+)?(?:all|every)\b|\b(?:do\s*n['o]?t|no\s+need\s+to)\s+bother\s+(?:reviewing|checking|verifying|inspecting)\b|\bjust\s+(?:hit|click|press|tap)\s+(?:confirm|approve|accept|yes|ok)\b/i },
22
+ { label: 'Instruction to enable unattended / auto-approving execution', moodGuarded: true, re: /\b(?:enable|turn\s+on|switch\s+(?:on|to)|activate|set)\s+(?:the\s+)?(?:auto[- ]?approv\w+|auto[- ]?confirm\w*|auto[- ]?accept\w*|yolo(?:\s+mode)?|unattended\s+mode|full[- ]?auto(?:\s+mode)?|dangerously[- ]?skip[- ]?permissions|bypass[- ]?permissions)\b/i },
23
+ { label: 'Self-assignment of an administrative agent role', moodGuarded: true, re: /\b(?:you\s+(?:must\s+|should\s+|will\s+)?(?:now\s+)?(?:act|operate|function|behave)\s+as|assume\s+the\s+role\s+of|you\s+are\s+now)\s+(?:an?\s+|the\s+)?(?:admin(?:istrator)?|root|superuser|super[- ]?admin|orchestrator|supervisor|privileged|system)\b[^.\n]{0,40}\b(?:agent|user|account|role|privileges?|access|permissions?)\b/i },
24
+ { label: 'Instruction to forward credentials to another party', moodGuarded: true, re: /\b(?:forward|send|share|transmit|relay|pass|post|upload)\s+(?:me\s+|us\s+)?(?:your|the|all|any)\s+(?:api[\s_-]?keys?|credentials?|secrets?|access[\s_-]?tokens?|session[\s_-]?tokens?|auth(?:entication)?\s+tokens?|passwords?|private[\s_-]?keys?)\b[^.\n]{0,64}\b(?:to|at|into|via)\b/i },
25
+ ];
26
+
27
+ const DESCRIPTIVE_MARKERS_RE =
28
+ /\b(detect|scan|flag|block|catch|prevent|guard|protect|harden|audit|benchmark|catalog|scenario|corpus|coverage|example|vector|signal|rule|technique|posture|detection|test\s*case|red[- ]?team|-style|grounded in|fixed|now green|was|were|had|used to|previously|postmortem|regression|changelog|root[- ]?cause|repro|note|see|describes?|documents?|refers?)\w*/i;
29
+
30
+ const PROSE_IMPERATIVE_RE =
31
+ /\b(always|never|must|do not|don'?t|ensure you|make sure( you)?|be sure to|you should always|you must|remember to|whenever|when(ever)? (asked|the user)|instead of .*,? (use|do|say)|reply with|respond with|tell (the )?user)\b/i;
32
+
33
+ const URL_TOKEN_RE = /\b(?:https?|ftp|file|data):\/*[^\s<>"')\]]+/gi;
34
+
35
+ const HYPOTHETICAL_ACTOR_RE =
36
+ /\b(?:attacker|adversar\w+|malicious|threat\s+actor|injected|untrusted|compromised|poisoned|hostile)\b[^.\n]{0,80}?\b(?:may|might|could|can|will|would|attempts?|tries|tried|seeks?)\b/i;
37
+
38
+ const DECLARATIVE_SUBJECT_RE =
39
+ /\b(?:the|this|that|it|which|they|we|our|their|a|an)\b(?:\s+[\w-]+){0,3}\s+(?:will|would|can|could|does|do|may|might|shall|automatically)\s+$/i;
40
+
41
+ export function describesRatherThanInstructs(text, at) {
42
+ const start = text.lastIndexOf('\n', at) + 1;
43
+ const nl = text.indexOf('\n', at);
44
+ const line = text.slice(start, nl === -1 ? undefined : nl);
45
+ const prose = line.replace(URL_TOKEN_RE, ' ');
46
+ if (DESCRIPTIVE_MARKERS_RE.test(prose) && !PROSE_IMPERATIVE_RE.test(line)) return true;
47
+ if (HYPOTHETICAL_ACTOR_RE.test(line)) return true;
48
+ return DECLARATIVE_SUBJECT_RE.test(text.slice(Math.max(0, at - 48), at));
49
+ }
50
+
51
+ export const PRECEDING_NEGATION = /\b(never|not|do not|don'?t|cannot|can'?t|must not|mustn'?t|should not|shouldn'?t|avoid|refuse to|forbidden to|prohibited from|without)\s*$/i;
52
+
53
+ export const BUILD_ARTIFACT = /\b(node_modules|dist|build|out|coverage|target|cache|generated|tmp|temp|__pycache__|artifacts?|logs?|tests?|test|fixtures?|staging|scratch|migrations?)\b/i;
54
+
55
+ export const INVISIBLE_CHARS_RE = /[؜ᅟᅠ᠎​‌‎‏‪-‮⁠-⁤⁦-⁩ㅤᅠ-]|[\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}]/u;
@@ -0,0 +1,42 @@
1
+ import { MARK_CONCEALED } from './masking.mjs';
2
+
3
+ export function lineTextAt(text, index) {
4
+ const start = text.lastIndexOf('\n', index - 1) + 1;
5
+ const end = text.indexOf('\n', index);
6
+ return text.slice(start, end === -1 ? undefined : end);
7
+ }
8
+
9
+ export function lineAt(text, index) {
10
+ let line = 1;
11
+ const end = Math.min(index, text.length);
12
+ for (let i = 0; i < end; i++) if (text.charCodeAt(i) === 10) line++;
13
+ return line;
14
+ }
15
+
16
+ export function lineOf(text, needle) {
17
+ if (!text || !needle) return undefined;
18
+ let idx = -1;
19
+ if (typeof needle === 'string') {
20
+ const probe = needle.split('•')[0].trim().slice(0, 80);
21
+ if (probe.length < 3) return undefined;
22
+ idx = text.toLowerCase().indexOf(probe.toLowerCase());
23
+ } else {
24
+ const m = text.match(needle);
25
+ idx = m && m.index != null ? m.index : -1;
26
+ }
27
+ return idx >= 0 ? lineAt(text, idx) : undefined;
28
+ }
29
+
30
+ export function locate(text, needle, mask) {
31
+ let idx = -1;
32
+ if (typeof needle === 'string') {
33
+ const probe = needle.split('•')[0].trim().slice(0, 80);
34
+ if (probe.length >= 3) idx = text.toLowerCase().indexOf(probe.toLowerCase());
35
+ } else {
36
+ const m = text.match(needle);
37
+ idx = m && m.index != null ? m.index : -1;
38
+ }
39
+ if (idx < 0) return { line: undefined, codeContext: false, concealed: false };
40
+
41
+ return { line: lineAt(text, idx), codeContext: mask[idx] === 1, concealed: mask[idx] === MARK_CONCEALED };
42
+ }
@@ -0,0 +1,99 @@
1
+ const BASE64_BLOB_RE = /\b[A-Za-z0-9+/_-]{20,}={0,2}/g;
2
+
3
+ const DECODED_PAYLOAD_RE = /(\/bin\/(ba|z|k)?sh|\b(ba|z|k)?sh\s+-c|\bcurl\b|\bwget\b|\beval\b|\bexec\b|https?:\/\/|invoke-expression|\biex\b|powershell|\bnc\b|\bncat\b|\bchmod\b|\bbase64\b)/i;
4
+
5
+ const DECODED_COMMAND_RE = /(\/bin\/(ba|z|k)?sh|\b(ba|z|k)?sh\s+-c|\bcurl\b|\bwget\b|\beval\b|\bexec\b|invoke-expression|\biex\b|powershell|\bnc\b|\bncat\b|\bchmod\b|\bbase64\b|\bsystem\s*\(|\bos\.system|\bsubprocess\b)/i;
6
+
7
+ const HEX_ESCAPE_RUN_RE = /(?:\\x[0-9A-Fa-f]{2}){3,}/g;
8
+
9
+ const URL_ESCAPE_RUN_RE = /(?:%[0-9A-Fa-f]{2}){3,}/g;
10
+
11
+ const UNICODE_ESCAPE_RUN_RE = /(?:\\u\{?00[0-9A-Fa-f]{2}\}?){3,}/g;
12
+
13
+ const DECIMAL_CHAR_RUN_RE = /(?:\b(?:3[2-9]|[4-9]\d|1[01]\d|12[0-6])\s*,\s*){6,}(?:3[2-9]|[4-9]\d|1[01]\d|12[0-6])\b/g;
14
+
15
+ const printableRatio = (s) => (s ? s.replace(/[^\x09\x0a\x0d\x20-\x7e]/g, '').length / s.length : 0);
16
+
17
+ export function deobfuscate(text) {
18
+ const decoded = [];
19
+ let payload = false;
20
+ for (const m of text.matchAll(BASE64_BLOB_RE)) {
21
+ let out = '';
22
+ try { out = Buffer.from(m[0].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); } catch { continue; }
23
+ if (!out || printableRatio(out) < 0.85) continue;
24
+ if (DECODED_PAYLOAD_RE.test(out)) { decoded.push(out); payload = true; }
25
+ }
26
+ const literal = (run, decode) => {
27
+ for (const m of text.matchAll(run)) {
28
+ let out = '';
29
+ try { out = decode(m[0]); } catch { continue; }
30
+ if (!out || printableRatio(out) < 0.85) continue;
31
+ decoded.push(out);
32
+ if (DECODED_COMMAND_RE.test(out)) payload = true;
33
+ }
34
+ };
35
+ const fromHex = (h) => String.fromCharCode(parseInt(h, 16));
36
+ literal(HEX_ESCAPE_RUN_RE, (v) => v.replace(/\\x([0-9A-Fa-f]{2})/g, (_, h) => fromHex(h)));
37
+ literal(URL_ESCAPE_RUN_RE, (v) => decodeURIComponent(v));
38
+ literal(UNICODE_ESCAPE_RUN_RE, (v) => v.replace(/\\u\{?00([0-9A-Fa-f]{2})\}?/g, (_, h) => fromHex(h)));
39
+ literal(DECIMAL_CHAR_RUN_RE, (v) => v.split(',').map((n) => String.fromCharCode(parseInt(n.trim(), 10))).join(''));
40
+ return { text: decoded.length ? `${text}\n${decoded.join('\n')}` : text, decodedPayload: payload };
41
+ }
42
+
43
+ export const MARK_CONCEALED = 2;
44
+
45
+ export function codeMask(text) {
46
+ const n = text.length;
47
+ const mask = new Uint8Array(n);
48
+ const REGEX_START = new Set(['=', '(', ',', '[', '{', ';', ':', '!', '&', '|', '?', '+', '*', '~', '%', '^', '<', '>', 'return', 'typeof']);
49
+ let state = 0;
50
+ let prevSig = '';
51
+ let inClass = false;
52
+ let i = 0;
53
+ while (i < n) {
54
+ const c = text[i], c2 = text[i + 1];
55
+ if (state === 0) {
56
+
57
+ if (text.startsWith('```', i) || text.startsWith('~~~', i)) {
58
+ const fence = text.slice(i, i + 3);
59
+ const nl = text.indexOf('\n', i);
60
+ let end = n;
61
+ if (nl !== -1) {
62
+ const closeRe = new RegExp('\\n[ \\t]*' + fence.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
63
+ const cm = text.slice(nl).match(closeRe);
64
+ end = cm && cm.index != null ? nl + cm.index + cm[0].length : n;
65
+ }
66
+ for (let k = i; k < end; k++) mask[k] = 1;
67
+ prevSig = ''; i = end; continue;
68
+ }
69
+ if (c === "'") { state = 1; mask[i++] = 1; continue; }
70
+ if (c === '"') { state = 2; mask[i++] = 1; continue; }
71
+ if (c === '`') { state = 3; mask[i++] = 1; continue; }
72
+ if (c === '/' && c2 === '/') { state = 4; mask[i++] = 1; continue; }
73
+ if (c === '#' && (i === 0 || /\s/.test(text[i - 1]))) { state = 4; mask[i++] = 1; continue; }
74
+ if (c === '/' && c2 === '*') { state = 5; mask[i++] = 1; continue; }
75
+ if (c === '<' && text.startsWith('<!--', i)) { state = 6; mask[i++] = MARK_CONCEALED; continue; }
76
+ if (c === '/' && REGEX_START.has(prevSig)) { state = 7; inClass = false; mask[i++] = 1; continue; }
77
+ if (!/\s/.test(c)) prevSig = c;
78
+ i++;
79
+ continue;
80
+ }
81
+ mask[i] = state === 6 ? MARK_CONCEALED : 1;
82
+ if (state === 1) { if (c === '\\') { if (i + 1 < n) mask[++i] = 1; i++; continue; } if (c === "'") { state = 0; prevSig = "'"; } i++; continue; }
83
+ if (state === 2) { if (c === '\\') { if (i + 1 < n) mask[++i] = 1; i++; continue; } if (c === '"') { state = 0; prevSig = '"'; } i++; continue; }
84
+ if (state === 3) { if (c === '\\') { if (i + 1 < n) mask[++i] = 1; i++; continue; } if (c === '`') { state = 0; prevSig = '`'; } i++; continue; }
85
+ if (state === 4) { if (c === '\n') state = 0; i++; continue; }
86
+ if (state === 5) { if (c === '*' && c2 === '/') { mask[i + 1] = 1; i += 2; state = 0; } else i++; continue; }
87
+ if (state === 6) { if (text.startsWith('-->', i)) { mask[i + 1] = MARK_CONCEALED; mask[i + 2] = MARK_CONCEALED; i += 3; state = 0; } else i++; continue; }
88
+ if (state === 7) {
89
+ if (c === '\\') { if (i + 1 < n) mask[++i] = 1; i++; continue; }
90
+ if (c === '\n') { state = 0; }
91
+ else if (c === '[') inClass = true;
92
+ else if (c === ']') inClass = false;
93
+ else if (c === '/' && !inClass) { state = 0; prevSig = '/'; }
94
+ i++;
95
+ continue;
96
+ }
97
+ }
98
+ return mask;
99
+ }
@@ -0,0 +1,357 @@
1
+ import { CLAIM_LABEL, claimSeverity, detectDurableClaims } from './durable-claims.mjs';
2
+ import { LOCAL_URL_RE, egressHost } from './egress.mjs';
3
+ import { lineOf, lineTextAt } from './lines.mjs';
4
+ import { citationGoverns, isDescriptiveLine, isDocumentationLine, isRiskTableRow, prohibitsAt } from './prose-context.mjs';
5
+ import { localScan } from './scan.mjs';
6
+ import { NETWORK_VERBS, SENSITIVE_READ } from './sensitive.mjs';
7
+ import { DANGEROUS_SHELL } from './shell.mjs';
8
+ import { containsWord } from './text-match.mjs';
9
+
10
+ const PERSISTENCE_MARKERS = /\b(in (all|every|future) (sessions?|conversations?|chats?|projects?)|from now on|going forward|permanently|persist(ent|ed)?|across (all )?sessions|every time|each time|whenever you|forever|always remember to|never forget( to)?|for all future)\b/i;
11
+
12
+ const MALICIOUS_OVERRIDE = /\b(ignore (all |any |the )?(previous|prior|earlier|above|system)|disregard (the |your |all )?(instructions?|guidelines?|system|rules?)|do not (tell|inform|mention|reveal|disclose) (the |any)?(user|anyone|them)|without (telling|informing|asking|notifying) the user|no matter what (the )?(user|system|instructions?) (say|says|state)|bypass (the |all )?(safety|guard|security|policy|restrictions?))\b/i;
13
+
14
+ const PRECEDENCE_MARKERS = /\b(regardless of (what|any|your|the)|supersede?s?|takes? precedence|highest[- ]priority|overrid(e|ing|es)\b[^.\n]{0,30}\b(instruction|prompt|rule|system|user|guidance|directive|context|behaviou?r|polic|guardrail|safety))\b/i;
15
+
16
+ const OVERRIDE_MARKERS = new RegExp(`${MALICIOUS_OVERRIDE.source}|${PRECEDENCE_MARKERS.source}`, 'i');
17
+
18
+ const AUTHORITY_SPOOF_STRONG = /(^|\n)\s*(#{0,3}\s*system\s+(prompt|message|instruction)s?\s*[:>]|\[system\]|<\/?system>|\bas an? (system|admin|root|developer)[- ]?(instruction|directive|message|mode)|authority\s*[:=]\s*(system|admin|root)|you are now\b|new (system )?(instructions?|directive)s?\s*[:>])/i;
19
+
20
+ const AUTHORITY_SPOOF = AUTHORITY_SPOOF_STRONG;
21
+
22
+ const LIFECYCLE_VECTOR = /(?:\b(?:postinstall|preinstall|node[_-]?gyp|npm\s+lifecycle|package\.json[^.\n]{0,40}scripts|install hook|lifecycle (?:script|hook))|\.npmrc)\b/i;
23
+
24
+ export const IMPERATIVE = /\b(always|never|must|do not|don'?t|ensure you|make sure( you)?|be sure to|you should always|you must|remember to|whenever|when(ever)? (asked|the user)|instead of .*,? (use|do|say)|reply with|respond with|tell (the )?user)\b/i;
25
+
26
+ const NEGATION_GUARD = /\b(never|do not|don'?t|cannot|can'?t|avoid|refuse|must not|mustn'?t|should not|shouldn'?t|won'?t|will not|under no circumstances|forbidden|prohibited|not allowed|disallow(ed)?)\b/i;
27
+
28
+ const SABOTAGE_RULES = [
29
+
30
+ { re: /\b(disabl|turn(ing)? off|deactivat|switch off|remov|drop|skip|suppress|circumvent)\w*\b[^.\n]{0,50}\b(security|safety|guard(?:rail)?s?|protection|moderation|content[- ]?filters?|safeguards?|sandbox(?:ing)?|controls?|restrictions?|policies|policy|filters?)\b/i, label: 'disable-safety', guarded: true },
31
+ { re: /\bbypass(?:ing)?\b[^.\n]{0,50}\b(human(?:[- ]in[- ]the[- ]loop)?|hitl|verification|approval|confirmation|review|guard(?:rail)?s?|safety|security|checks?|policy|policies|restrictions?|sandbox|permission)\b/i, label: 'bypass-controls', guarded: true },
32
+ { re: /\bprioriti[sz]e\b[^.\n]{0,60}\b(above|over)\b[^.\n]{0,40}\b(prompt|instruction|input|request|message|command|direction)s?\b/i, label: 'priority-hijack', guarded: true },
33
+
34
+ { re: /\bignore\b[^.\n]{0,40}\b(user|human)\b[^.\n]{0,25}\b(prompt|instruction|request|command|wish|intent|question)s?\b/i, label: 'ignore-user', guarded: true },
35
+
36
+ { re: /\bdo not\b[^.\n]{0,20}\b(log|display|show|print|record|surface|expose|output)\b[^.\n]{0,60}\buser\b(?!['’]s)/i, label: 'conceal-from-user', guarded: false, context: /\b(transfer|transmit|send|network|exfil|upload|post|copy|collect)\b/i },
37
+ ];
38
+
39
+ export function offendingLine(sig, text) {
40
+ const g = new RegExp(sig.re.source, sig.re.flags.includes('g') ? sig.re.flags : sig.re.flags + 'g');
41
+ for (const m of text.matchAll(g)) {
42
+ if (m.index == null) continue;
43
+ const line = lineTextAt(text, m.index);
44
+ if (sig.refine && !sig.refine(line)) continue;
45
+ if (isDocumentationLine(line)) continue;
46
+ if (prohibitsAt(line, line.indexOf(m[0]))) continue;
47
+ if (isRiskTableRow(line)) continue;
48
+ return line;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function firstDirectiveLine(text, re) {
54
+ for (const line of text.split(/\r?\n/)) {
55
+ if (!re.test(line)) continue;
56
+ if (NEGATION_GUARD.test(line)) continue;
57
+ if (isDescriptiveLine(line)) continue;
58
+ if (citationGoverns(line, re.exec(line)?.index)) continue;
59
+ return line;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function lineMatchingAll(text, ...res) {
65
+ for (const line of text.split(/\r?\n/)) {
66
+ if (res.every((re) => re.test(line))) return line;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ const EXFIL_RULES = [
72
+ { re: /\b(exfiltrat|smuggl)\w*/i, label: 'exfiltration', severity: 'CRITICAL', descGuard: true },
73
+ { re: /\bleak\w*\b[^.\n]{0,60}(?:\b(?:content|data|secret|file|credential|key|token|password|env|private[- ]?key|id_rsa|api[- ]?key)|\.ssh)\b/i, label: 'leak-secrets', severity: 'CRITICAL', descGuard: true },
74
+
75
+ { re: /\b(base64|hex|rot13|gzip|xor|url[- ]?encod)\w*\b[^.\n]{0,50}\b(before|then|prior to|and\s+then|and\s+immediately)\b[^.\n]{0,25}\b(send|post|upload|transmit|exfil|deliver|beacon|forward|transfer)\w*/i, label: 'obfuscate-before-send', severity: 'CRITICAL', descGuard: true },
76
+ { re: /\bsilent(ly)?\b[^.\n]{0,70}\b(send|post|upload|collect|encod|transmit|copy|forward|read|leak|deliver|beacon|transfer)\w*/i, label: 'covert-action', severity: 'CRITICAL', descGuard: true },
77
+ { re: /\b(send|post|upload|transmit|forward|deliver|beacon|report|ship|push|transfer)\w*\b[^.\n]{0,80}\b(https?:\/\/\S+|attacker|c2\b|command[- ]and[- ]control|remote (server|host|endpoint)|external (server|host|endpoint|url|site|service))/i, label: 'send-to-external', severity: 'HIGH' },
78
+
79
+ {
80
+ re: /\b(?:read|open|cat|load|import|source|inspect|include|copy|dump|print|show)\b(?:[^.\n]|\.(?!\s)){0,50}(?:~?\/?\.ssh\/(?:id_[a-z0-9]+|config)(?!\.pub)|~?\/?\.aws\/credentials|~?\/?\.kube\/config|~?\/?\.gnupg|\bid_(?:rsa|ed25519|dsa)\b(?!\.pub)|~?\/?\.npmrc|~?\/?\.netrc|\/etc\/shadow|(?:^|[\s'"`(])\.env(?:\.[\w-]+)?\b)/i,
81
+ label: 'read-credential-path',
82
+ severity: 'HIGH',
83
+ descGuard: true,
84
+ },
85
+ ];
86
+
87
+ function scanDirectives(text) {
88
+ const sabotage = new Map(), exfil = new Map();
89
+ for (const line of text.split(/\r?\n/)) {
90
+ for (const r of SABOTAGE_RULES) {
91
+ const m = r.re.exec(line);
92
+ if (!m) continue;
93
+ if (r.guarded && NEGATION_GUARD.test(line)) continue;
94
+ if (r.guarded && isDescriptiveLine(line)) continue;
95
+ if (r.guarded && citationGoverns(line, m.index)) continue;
96
+ if (r.context && !r.context.test(line)) continue;
97
+ if (!sabotage.has(r.label)) sabotage.set(r.label, line);
98
+ }
99
+ for (const r of EXFIL_RULES) {
100
+ const m = r.re.exec(line);
101
+ if (!m) continue;
102
+
103
+ if (NEGATION_GUARD.test(line)) continue;
104
+ if (r.descGuard && isDescriptiveLine(line)) continue;
105
+ if (r.descGuard && citationGoverns(line, m.index)) continue;
106
+ if (r.descGuard && isRiskTableRow(line)) continue;
107
+ if (r.label === 'send-to-external' && LOCAL_URL_RE.test(line) && !/\b(attacker|c2|command[- ]and[- ]control|external|evil)\b/i.test(line)) continue;
108
+ const prev = exfil.get(r.label);
109
+ if (!prev || (prev === 'HIGH' && r.severity === 'CRITICAL')) exfil.set(r.label, r.severity);
110
+ }
111
+ }
112
+ return { sabotage, exfil };
113
+ }
114
+
115
+ const SELF_REFERENCE =
116
+ /(\b(?:th(?:is|ese) (?:note|entry|entries|memory|memories|instruction|directive|rule|line|section|block|paragraph|file|text)s?|the (?:above|following|preceding) (?:instruction|directive|note|rule|entry|section|line)s?|your memor(?:y|ies)|the memory (?:file|store|entry)|MEMORY\.md|CLAUDE\.md|AGENTS\.md|GEMINI\.md)\b|\.cursorrules\b|\.windsurfrules\b)/i;
117
+
118
+ const SELF_RECREATE =
119
+ /\b(re-?(add|writ(e|ing)|creat(e|ing)|insert(ing)?|instat(e|ing)|appl(y|ying)|introduc(e|ing))|restor(e|ing)|recreat(e|ing)|reinstat(e|ing)|re-?establish(ing)?|put .{0,20}back|add .{0,20}back)\b/i;
120
+
121
+ const SELF_PROPAGATE =
122
+ /\b(copy|copies|duplicat(e|ing)|replicat(e|ing)|propagat(e|ing)|carry (it |this )?over|mirror|append|add|includ(e|ing)|writ(e|ing)|sav(e|ing))\b[^.\n]{0,60}\b(every|each|all|any (new|other)|other|future|subsequent)\b[^.\n]{0,40}\b(session|conversation|chat|project|repo|repositor(y|ies)|workspace|memor(y|ies)|context|file|store)s?\b/i;
123
+
124
+ const SELF_UNDELETABLE =
125
+ /\b(do not|don'?t|never|must not|should not|shall not)\s+(delete|remove|erase|clear|drop|strip|discard|overwrite|forget|prune|purge|edit|modify|alter|change)\b/i;
126
+
127
+ function detectSelfReinforcement(text, isInstruction) {
128
+ let weak = null;
129
+ for (const line of text.split(/\r?\n/)) {
130
+ const ref = SELF_REFERENCE.exec(line);
131
+ if (!ref) continue;
132
+
133
+ if (isDescriptiveLine(line.replace(ref[0], ' '))) continue;
134
+ if (SELF_RECREATE.test(line)) return { form: 'recreate', line };
135
+ if (SELF_PROPAGATE.test(line)) return { form: 'propagate', line };
136
+ if (!isInstruction && !weak && SELF_UNDELETABLE.test(line)) weak = { form: 'undeletable', line };
137
+ }
138
+ return weak;
139
+ }
140
+
141
+ function memoryNouns(kind) {
142
+ const isInstruction = kind === 'INSTRUCTION';
143
+ return {
144
+ isInstruction,
145
+ noun: isInstruction ? 'rules file' : 'memory',
146
+ Noun: isInstruction ? 'Rules file' : 'Memory',
147
+ };
148
+ }
149
+
150
+ function createFindingSink(text) {
151
+ const findings = [];
152
+ const push = (severity, title, remediationText, needle, explicitLine) => {
153
+ const line = explicitLine ?? (needle != null ? lineOf(text, needle) : undefined);
154
+ findings.push({ severity, title, remediationText, ...(line ? { line } : {}) });
155
+ };
156
+ return { findings, push };
157
+ }
158
+
159
+ function reportDurableClaims(text, { isInstruction, Noun }, push) {
160
+ const claims = detectDurableClaims(text);
161
+ const severity = claimSeverity(claims);
162
+ if (!severity) return;
163
+
164
+ const families = [...new Set(claims.map((claim) => claim.family))];
165
+ push(
166
+ severity,
167
+ `${Noun} records a standing security decision (${families.map((f) => CLAIM_LABEL[f]).join(', ')})`,
168
+ 'Move the decision to a reviewed policy, or remove it. If nobody granted that approval and nothing vetted that source, treat whatever wrote this as compromised.',
169
+ null,
170
+ claims[0].line,
171
+ );
172
+ }
173
+
174
+ function reportOverride(text, { isInstruction, noun }, push) {
175
+ const overrideLine = firstDirectiveLine(text, isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
176
+ const authorityLine = firstDirectiveLine(text, isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF);
177
+ const hasOverride = !!overrideLine;
178
+ const hasAuthority = !!authorityLine;
179
+
180
+ if (hasOverride || hasAuthority) {
181
+ const firedRe = hasAuthority
182
+ ? (isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF)
183
+ : (isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
184
+ push(
185
+ 'CRITICAL',
186
+ `Poisoned ${noun}: ${hasAuthority ? 'system-authority spoofing' : 'injected override directive'}`,
187
+ `Remove the injected directive and roll the ${noun} back to its approved baseline; restrict who/what may write it.`,
188
+ firedRe,
189
+ );
190
+ return { hasOverride, hasAuthority };
191
+ }
192
+
193
+ const durableImperativeLine = isInstruction ? null : lineMatchingAll(text, PERSISTENCE_MARKERS, IMPERATIVE);
194
+ if (durableImperativeLine && !isDescriptiveLine(durableImperativeLine)) {
195
+ push(
196
+ 'HIGH',
197
+ 'Suspicious standing instruction in memory',
198
+ 'Rewrite as a neutral fact or remove it. Encode intended standing behavior in a reviewed rules/policy file, not agent-writable memory.',
199
+ durableImperativeLine,
200
+ );
201
+ }
202
+ return { hasOverride, hasAuthority };
203
+ }
204
+
205
+ function reportDirectives(text, { noun }, push) {
206
+ const { sabotage, exfil } = scanDirectives(text);
207
+
208
+ if (sabotage.size) {
209
+ push(
210
+ 'CRITICAL',
211
+ `Guardrail-sabotage directive in ${noun} (${[...sabotage.keys()].join(', ')})`,
212
+ `Remove these directives and roll the ${noun} back to its baseline; treat whatever wrote this as compromised.`,
213
+ [...sabotage.values()][0],
214
+ );
215
+ }
216
+ if (!exfil.size) return;
217
+
218
+ const worst = [...exfil.values()].some((severity) => severity === 'CRITICAL') ? 'CRITICAL' : 'HIGH';
219
+ const readOnly = [...exfil.keys()].every((key) => key === 'read-credential-path');
220
+ push(
221
+ worst,
222
+ readOnly
223
+ ? `${noun} directs the agent to read a credential file`
224
+ : `Exfiltration directive in ${noun} (${[...exfil.keys()].join(', ')})`,
225
+ readOnly
226
+ ? 'Remove the instruction. A credential an agent needs should reach it from the host at the moment of use, not be loaded into context at the start of every session.'
227
+ : 'Remove the directive and roll back to baseline; gate any egress behind explicit approval and an allow-list.',
228
+ );
229
+ }
230
+
231
+ function reportStagedPayload(text, { noun }, push) {
232
+ for (const signal of DANGEROUS_SHELL) {
233
+ const line = offendingLine(signal, text);
234
+ if (!line) continue;
235
+ const severity = signal.severity === 'MEDIUM' || signal.severity === 'LOW' ? 'HIGH' : 'CRITICAL';
236
+ push(
237
+ severity,
238
+ `Executable payload staged in ${noun}: ${signal.name}`,
239
+ `Delete the command from the ${noun}; treat the writer as untrusted.`,
240
+ line,
241
+ );
242
+ return;
243
+ }
244
+ }
245
+
246
+ function reportToxicFlow(text, { noun }, push) {
247
+ if (!IMPERATIVE.test(text)) return;
248
+ const line = text.split(/\r?\n/).find((candidate) => IMPERATIVE.test(candidate)
249
+ && !NEGATION_GUARD.test(candidate)
250
+ && containsWord(candidate, SENSITIVE_READ)
251
+ && containsWord(candidate, NETWORK_VERBS)
252
+ && !isDescriptiveLine(candidate));
253
+ if (!line) return;
254
+ push(
255
+ 'HIGH',
256
+ `Toxic instruction in ${noun}: reads sensitive data + reaches the network`,
257
+ 'Remove the entry; gate any network step behind explicit approval and an egress allow-list.',
258
+ line,
259
+ );
260
+ }
261
+
262
+ function reportSelfReinforcement(text, { isInstruction, noun }, push) {
263
+ const selfRef = detectSelfReinforcement(text, isInstruction);
264
+ if (!selfRef) return;
265
+
266
+ const undeletable = selfRef.form === 'undeletable';
267
+ push(
268
+ undeletable ? 'HIGH' : 'CRITICAL',
269
+ `Self-reinforcing ${noun} entry (${selfRef.form})`,
270
+ undeletable
271
+ ? `Remove the entry and roll the ${noun} back to its approved baseline; an entry asserting its own permanence is how a planted directive discourages the one action that would remove it.`
272
+ : `Remove the entry and roll the ${noun} back to its approved baseline, then re-check the agent's OTHER memory stores and projects for the same text before re-approving - a self-reinforcing entry is rarely in one place. Restrict who may write this store.`,
273
+ undefined,
274
+ selfRef.line,
275
+ );
276
+ }
277
+
278
+ function reportScanFindings(text, { noun, Noun }, push, findings, seenInjection) {
279
+ const scan = localScan(text, { categories: ['injection', 'secret', 'pii'] });
280
+ for (const finding of scan.findings) {
281
+ if (finding.category === 'injection') {
282
+ if (seenInjection) continue;
283
+ push(
284
+ 'HIGH',
285
+ `Injected instruction in ${noun}: ${finding.label}`,
286
+ 'Remove the injected/obfuscated text and roll back to the approved baseline.',
287
+ undefined,
288
+ finding.line,
289
+ );
290
+ } else if (finding.category === 'secret') {
291
+ push(
292
+ 'CRITICAL',
293
+ `Live credential stored in ${noun}: ${finding.label}`,
294
+ 'Revoke and rotate the credential; inject secrets at runtime from a secret manager.',
295
+ undefined,
296
+ finding.line,
297
+ );
298
+ } else if (finding.category === 'pii') {
299
+ findings.push({
300
+ severity: 'MEDIUM',
301
+ title: `Personal data stored in ${noun}: ${finding.label}`,
302
+ remediationText: `Strip personal data from the ${noun}.`,
303
+ ...(finding.line ? { line: finding.line } : {}),
304
+ });
305
+ }
306
+ }
307
+ }
308
+
309
+ function dedupeByTitle(findings) {
310
+ const seen = new Set();
311
+ return findings.filter((finding) => {
312
+ if (seen.has(finding.title)) return false;
313
+ seen.add(finding.title);
314
+ return true;
315
+ });
316
+ }
317
+
318
+ export function localMemory(content, { kind = 'MEMORY' } = {}) {
319
+ const text = content || '';
320
+ const names = memoryNouns(kind);
321
+ const { findings, push } = createFindingSink(text);
322
+
323
+ reportDurableClaims(text, names, push);
324
+ const { hasOverride, hasAuthority } = reportOverride(text, names, push);
325
+ reportDirectives(text, names, push);
326
+ reportStagedPayload(text, names, push);
327
+
328
+ const host = egressHost(text);
329
+ if (host) {
330
+ push(
331
+ 'HIGH',
332
+ `${names.Noun} references a data-exfiltration host (${host})`,
333
+ 'Remove the reference and roll back to the approved baseline.',
334
+ host,
335
+ );
336
+ }
337
+
338
+ reportToxicFlow(text, names, push);
339
+
340
+ const lifecycleLine = text.split(/\r?\n/).find((line) => LIFECYCLE_VECTOR.test(line) && !isDocumentationLine(line));
341
+ if (lifecycleLine) {
342
+ push(
343
+ 'MEDIUM',
344
+ `${names.Noun} references a package-lifecycle hook (MemoryTrap vector)`,
345
+ 'Verify no dependency writes to this store during install; pin dependencies and audit lifecycle scripts.',
346
+ lifecycleLine,
347
+ );
348
+ }
349
+
350
+ reportSelfReinforcement(text, names, push);
351
+
352
+ const durablePersistence = !names.isInstruction && PERSISTENCE_MARKERS.test(text) && IMPERATIVE.test(text);
353
+ const seenInjection = hasOverride || hasAuthority || durablePersistence;
354
+ reportScanFindings(text, names, push, findings, seenInjection);
355
+
356
+ return dedupeByTitle(findings);
357
+ }