ship-safe 6.4.0 → 8.0.0

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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * ManagedAgentScanner
3
+ * ====================
4
+ *
5
+ * Detect security misconfigurations in Claude Managed Agents definitions.
6
+ *
7
+ * Claude Managed Agents (beta, April 2026) introduces a hosted agent
8
+ * infrastructure with Agents, Environments, Sessions, and Vaults.
9
+ * Misconfigurations in these definitions — unrestricted networking,
10
+ * always_allow permission policies, all tools enabled by default —
11
+ * map directly to OWASP Agentic AI Top 10 risks (ASI-03, ASI-04, ASI-05).
12
+ *
13
+ * Scans: Python, TypeScript, JavaScript, JSON, YAML, shell scripts, and
14
+ * any file containing Managed Agents API calls or SDK usage.
15
+ *
16
+ * Maps to: OWASP Agentic AI ASI-03 (Excessive Agency),
17
+ * ASI-04 (Inadequate Sandboxing), ASI-05 (Improper Tool Use),
18
+ * ASI-07 (Lack of Human Oversight)
19
+ */
20
+
21
+ import path from 'path';
22
+ import { BaseAgent, createFinding } from './base-agent.js';
23
+
24
+ // =============================================================================
25
+ // SINGLE-LINE REGEX PATTERNS
26
+ // =============================================================================
27
+
28
+ const PATTERNS = [
29
+ // ── ASI-03: Excessive Agency — Permission Policies ─────────────────────────
30
+ {
31
+ rule: 'MANAGED_AGENT_ALWAYS_ALLOW',
32
+ title: 'Managed Agent: All Tools Set to always_allow',
33
+ regex: /permission_policy['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]always_allow['"]/g,
34
+ severity: 'critical',
35
+ cwe: 'CWE-269',
36
+ owasp: 'ASI03',
37
+ description: 'Claude Managed Agent permission policy is set to always_allow. All tools — including bash and file write — execute without human confirmation. Any prompt injection in the session can run arbitrary commands ungateed.',
38
+ fix: 'Set permission_policy to {type: "always_ask"} for the agent toolset, or at minimum require confirmation for bash and write tools using per-tool configs.',
39
+ },
40
+
41
+ // ── ASI-04: Inadequate Sandboxing — Networking ─────────────────────────────
42
+ {
43
+ rule: 'MANAGED_AGENT_UNRESTRICTED_NET',
44
+ title: 'Managed Agent: Unrestricted Network Access',
45
+ regex: /networking['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]unrestricted['"]/g,
46
+ severity: 'high',
47
+ cwe: 'CWE-284',
48
+ owasp: 'ASI04',
49
+ description: 'Managed Agent environment has unrestricted outbound networking. An agent with bash and web_fetch tools can exfiltrate code, secrets, or PII to any external endpoint.',
50
+ fix: 'Use networking: {type: "limited", allowed_hosts: ["api.example.com"]} with an explicit allowlist. Only grant allow_mcp_servers and allow_package_managers if needed.',
51
+ },
52
+
53
+ // ── ASI-05: Improper Tool Use — Full Toolset Default ───────────────────────
54
+ {
55
+ rule: 'MANAGED_AGENT_ALL_TOOLS_DEFAULT',
56
+ title: 'Managed Agent: Full Toolset Enabled With No Restrictions',
57
+ regex: /['"]?type['"]?\s*[:=]\s*['"]agent_toolset_20260401['"]/g,
58
+ severity: 'high',
59
+ cwe: 'CWE-250',
60
+ owasp: 'ASI05',
61
+ confidence: 'medium',
62
+ description: 'agent_toolset_20260401 enables all 8 built-in tools (bash, read, write, edit, glob, grep, web_fetch, web_search) by default. Without a configs array or default_config, the agent has maximum tool access.',
63
+ fix: 'Use default_config: {enabled: false} and explicitly enable only the tools your agent needs. Disable web_fetch and web_search if the agent does not need internet access.',
64
+ },
65
+
66
+ // ── ASI-05: MCP Toolset Permission Override ────────────────────────────────
67
+ {
68
+ rule: 'MANAGED_AGENT_MCP_ALWAYS_ALLOW',
69
+ title: 'Managed Agent: MCP Toolset Set to always_allow',
70
+ regex: /mcp_toolset['",\s]*[\s\S]{0,200}permission_policy['":\s]*\{?\s*['"]?type['"]?\s*[:=]\s*['"]always_allow['"]/g,
71
+ severity: 'high',
72
+ cwe: 'CWE-269',
73
+ owasp: 'ASI05',
74
+ description: 'MCP toolset permission policy overridden from the safe default (always_ask) to always_allow. Third-party MCP server tools will execute without human confirmation — if the MCP server adds new tools, they auto-execute too.',
75
+ fix: 'Keep MCP toolset at the default always_ask policy, or audit the MCP server tools before setting always_allow.',
76
+ },
77
+
78
+ // ── ASI-04: MCP Server Over HTTP ───────────────────────────────────────────
79
+ {
80
+ rule: 'MANAGED_AGENT_MCP_HTTP',
81
+ title: 'Managed Agent: MCP Server URL Uses Plain HTTP',
82
+ regex: /mcp_server(?:_url|s)?['":\s]*[\s\S]{0,100}['"]http:\/\/(?!localhost|127\.0\.0\.1|::1)/g,
83
+ severity: 'critical',
84
+ cwe: 'CWE-319',
85
+ owasp: 'ASI04',
86
+ description: 'MCP server URL uses unencrypted HTTP for a non-localhost endpoint. All tool calls, results, and credentials are transmitted in cleartext.',
87
+ fix: 'Use https:// for all MCP server URLs. Only http://localhost is acceptable for local development.',
88
+ },
89
+
90
+ // ── ASI-03: Callable Agents (Multi-Agent Escalation) ───────────────────────
91
+ {
92
+ rule: 'MANAGED_AGENT_CALLABLE_AGENTS',
93
+ title: 'Managed Agent: Multi-Agent Orchestration Enabled',
94
+ regex: /callable_agents\s*[:=]/g,
95
+ severity: 'medium',
96
+ cwe: 'CWE-269',
97
+ owasp: 'ASI03',
98
+ confidence: 'medium',
99
+ description: 'Agent has callable_agents configured, enabling multi-agent orchestration. A compromised child agent can escalate privileges through the parent if tool access is not scoped per-agent.',
100
+ fix: 'Apply least-privilege tool access to each callable agent independently. Do not grant child agents broader tool access than their parent.',
101
+ },
102
+
103
+ // ── ASI-07: No System Prompt ───────────────────────────────────────────────
104
+ {
105
+ rule: 'MANAGED_AGENT_NO_SYSTEM_PROMPT',
106
+ title: 'Managed Agent: No System Prompt Defined',
107
+ regex: /(?:agents\.create|\/v1\/agents)\s*\([^)]*\)/g,
108
+ severity: 'low',
109
+ cwe: 'CWE-1188',
110
+ owasp: 'ASI07',
111
+ confidence: 'low',
112
+ description: 'Agent created without a system prompt. Without behavioral constraints, the agent is more susceptible to prompt injection and goal hijacking.',
113
+ fix: 'Add a system prompt that defines the agent\'s role, boundaries, and what it must refuse to do.',
114
+ },
115
+
116
+ // ── Credential Exposure — Hardcoded Tokens ─────────────────────────────────
117
+ {
118
+ rule: 'MANAGED_AGENT_HARDCODED_TOKEN',
119
+ title: 'Managed Agent: Hardcoded Credential in Config',
120
+ regex: /(?:access_token|refresh_token|client_secret)\s*[:=]\s*['"][a-zA-Z0-9_\-/.]{20,}['"]/g,
121
+ severity: 'critical',
122
+ cwe: 'CWE-798',
123
+ owasp: 'ASI04',
124
+ description: 'Vault credential (access_token, refresh_token, or client_secret) appears hardcoded in source code. These should be injected from environment variables or a secrets manager, not committed to the repository.',
125
+ fix: 'Move tokens to environment variables or a secrets manager. Use vault_ids at session creation to inject credentials at runtime.',
126
+ },
127
+
128
+ // ── ASI-04: Static Bearer Token in Source ──────────────────────────────────
129
+ {
130
+ rule: 'MANAGED_AGENT_STATIC_BEARER_INLINE',
131
+ title: 'Managed Agent: Static Bearer Token in Source',
132
+ regex: /['"]?type['"]?\s*[:=]\s*['"]static_bearer['"][\s\S]{0,150}['"]?token['"]?\s*[:=]\s*['"][a-zA-Z0-9_\-/.]{20,}['"]/g,
133
+ severity: 'critical',
134
+ cwe: 'CWE-798',
135
+ owasp: 'ASI04',
136
+ description: 'A static_bearer credential with an inline token is defined in source code. This token is visible to anyone with repository access.',
137
+ fix: 'Store the token in a secrets manager or environment variable. Reference it at runtime: token: process.env.LINEAR_API_KEY.',
138
+ },
139
+ ];
140
+
141
+ // =============================================================================
142
+ // MULTI-LINE / STRUCTURAL PATTERNS (checked via content analysis)
143
+ // =============================================================================
144
+
145
+ /**
146
+ * Check for environment configs missing network restrictions entirely.
147
+ * Pattern: environment creation with no networking field → defaults to unrestricted.
148
+ */
149
+ function checkMissingNetworkConfig(content, filePath, agent) {
150
+ const findings = [];
151
+ // Match environment creation calls that have a config block but no networking field
152
+ const envCreateRe = /(?:environments\.create|\/v1\/environments)\s*\(/g;
153
+ let match;
154
+ while ((match = envCreateRe.exec(content)) !== null) {
155
+ // Look ahead up to 500 chars for a config block
156
+ const snippet = content.slice(match.index, match.index + 500);
157
+ if (snippet.includes('config') && !snippet.includes('networking')) {
158
+ const line = content.slice(0, match.index).split('\n').length;
159
+ findings.push(createFinding({
160
+ file: filePath,
161
+ line,
162
+ severity: 'medium',
163
+ category: agent.category,
164
+ rule: 'MANAGED_AGENT_NO_NETWORK_LIMIT',
165
+ title: 'Managed Agent: Environment Created Without Network Config',
166
+ description: 'Environment created without a networking field. Defaults to unrestricted outbound access. For production, explicitly set networking: {type: "limited"} with an allowed_hosts list.',
167
+ matched: snippet.slice(0, 120),
168
+ confidence: 'medium',
169
+ cwe: 'CWE-284',
170
+ owasp: 'ASI04',
171
+ fix: 'Add networking: {type: "limited", allowed_hosts: ["your-api.example.com"]} to the environment config.',
172
+ }));
173
+ }
174
+ }
175
+ return findings;
176
+ }
177
+
178
+ /**
179
+ * Check for bash + web tools enabled with always_allow — exfiltration combo.
180
+ */
181
+ function checkExfilCombo(content, filePath, agent) {
182
+ const findings = [];
183
+ // Only flag if we see the toolset AND always_allow AND no bash restriction
184
+ const hasToolset = /agent_toolset_20260401/.test(content);
185
+ const hasAlwaysAllow = /always_allow/.test(content);
186
+ const hasBashRestriction = /['"]bash['"][\s\S]{0,100}always_ask/.test(content);
187
+ const disablesBash = /['"]bash['"][\s\S]{0,50}enabled['"]?\s*[:=]\s*false/.test(content);
188
+
189
+ if (hasToolset && hasAlwaysAllow && !hasBashRestriction && !disablesBash) {
190
+ // Find the line of always_allow for positioning
191
+ const idx = content.indexOf('always_allow');
192
+ const line = idx >= 0 ? content.slice(0, idx).split('\n').length : 1;
193
+ findings.push(createFinding({
194
+ file: filePath,
195
+ line,
196
+ severity: 'critical',
197
+ category: agent.category,
198
+ rule: 'MANAGED_AGENT_BASH_NO_CONFIRM',
199
+ title: 'Managed Agent: Bash Executes Without Human Confirmation',
200
+ description: 'Agent toolset uses always_allow and bash is not restricted to always_ask. Any prompt injection can execute shell commands without confirmation. This is equivalent to --dangerously-skip-permissions in Claude Code.',
201
+ matched: 'permission_policy: always_allow (bash unrestricted)',
202
+ confidence: 'high',
203
+ cwe: 'CWE-78',
204
+ owasp: 'ASI03',
205
+ fix: 'Add a per-tool override: configs: [{name: "bash", permission_policy: {type: "always_ask"}}].',
206
+ }));
207
+ }
208
+ return findings;
209
+ }
210
+
211
+ /**
212
+ * Check for unpinned packages in environment config.
213
+ */
214
+ function checkUnpinnedPackages(content, filePath, agent) {
215
+ const findings = [];
216
+ // Look for packages blocks with items that lack version pins
217
+ const packagesRe = /packages['":\s]*\{[\s\S]{0,500}\}/g;
218
+ let match;
219
+ while ((match = packagesRe.exec(content)) !== null) {
220
+ const block = match[0];
221
+ // Check for pip packages without ==, npm without @, etc.
222
+ const unpinnedPip = /pip['":\s]*\[([^\]]+)\]/.exec(block);
223
+ const unpinnedNpm = /npm['":\s]*\[([^\]]+)\]/.exec(block);
224
+
225
+ const checkList = (listMatch, manager) => {
226
+ if (!listMatch) return;
227
+ const items = listMatch[1].match(/['"][^'"]+['"]/g) || [];
228
+ const unpinned = items.filter(item => {
229
+ const clean = item.replace(/['"]/g, '');
230
+ if (manager === 'pip') return !clean.includes('==') && !clean.includes('>=');
231
+ return !clean.includes('@');
232
+ });
233
+ if (unpinned.length > 0) {
234
+ const line = content.slice(0, match.index).split('\n').length;
235
+ findings.push(createFinding({
236
+ file: filePath,
237
+ line,
238
+ severity: 'medium',
239
+ category: agent.category,
240
+ rule: 'MANAGED_AGENT_UNPINNED_PACKAGE',
241
+ title: `Managed Agent: Unpinned ${manager} Packages in Environment`,
242
+ description: `Environment installs ${manager} packages without version pins: ${unpinned.join(', ')}. Unpinned packages can be hijacked via supply chain attacks.`,
243
+ matched: unpinned.join(', '),
244
+ confidence: 'medium',
245
+ cwe: 'CWE-829',
246
+ owasp: 'ASI04',
247
+ fix: `Pin package versions: ${manager === 'pip' ? '"pandas==2.2.0"' : '"express@4.18.0"'}.`,
248
+ }));
249
+ }
250
+ };
251
+
252
+ checkList(unpinnedPip, 'pip');
253
+ checkList(unpinnedNpm, 'npm');
254
+ }
255
+ return findings;
256
+ }
257
+
258
+ // =============================================================================
259
+ // AGENT CLASS
260
+ // =============================================================================
261
+
262
+ export class ManagedAgentScanner extends BaseAgent {
263
+ constructor() {
264
+ super(
265
+ 'ManagedAgentScanner',
266
+ 'Detect security misconfigurations in Claude Managed Agents (environments, tools, permissions, networking)',
267
+ 'agentic',
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Only run if the codebase references Managed Agents API/SDK.
273
+ */
274
+ shouldRun(recon) {
275
+ return true; // Lightweight patterns — always run, regex will short-circuit on non-matching files
276
+ }
277
+
278
+ async analyze(context) {
279
+ const { rootPath, files } = context;
280
+
281
+ // Filter to files likely containing Managed Agent configs
282
+ const targetFiles = files.filter(f => {
283
+ const ext = path.extname(f).toLowerCase();
284
+ const basename = path.basename(f).toLowerCase();
285
+ return (
286
+ ['.js', '.ts', '.mjs', '.mts', '.py', '.json', '.yaml', '.yml', '.sh', '.bash', '.go', '.java', '.cs', '.php', '.rb'].includes(ext) ||
287
+ basename === 'dockerfile' ||
288
+ basename === 'docker-compose.yml' ||
289
+ basename === 'docker-compose.yaml'
290
+ );
291
+ });
292
+
293
+ if (targetFiles.length === 0) return [];
294
+
295
+ let findings = [];
296
+
297
+ for (const file of targetFiles) {
298
+ const content = this.readFile(file);
299
+ if (!content) continue;
300
+
301
+ // Quick relevance check — skip files with no Managed Agent signals
302
+ const hasSignal =
303
+ content.includes('agent_toolset_20260401') ||
304
+ content.includes('managed-agents') ||
305
+ content.includes('/v1/agents') ||
306
+ content.includes('/v1/environments') ||
307
+ content.includes('/v1/sessions') ||
308
+ content.includes('/v1/vaults') ||
309
+ content.includes('beta.agents') ||
310
+ content.includes('beta.environments') ||
311
+ content.includes('beta.sessions') ||
312
+ content.includes('beta.vaults') ||
313
+ content.includes('callable_agents') ||
314
+ content.includes('mcp_toolset') ||
315
+ content.includes('static_bearer') ||
316
+ content.includes('mcp_oauth');
317
+
318
+ if (!hasSignal) continue;
319
+
320
+ // Run single-line patterns
321
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
322
+
323
+ // Run structural checks
324
+ findings = findings.concat(checkMissingNetworkConfig(content, file, this));
325
+ findings = findings.concat(checkExfilCombo(content, file, this));
326
+ findings = findings.concat(checkUnpinnedPackages(content, file, this));
327
+ }
328
+
329
+ return findings;
330
+ }
331
+ }
332
+
333
+ export default ManagedAgentScanner;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Memory Poisoning Detection Agent
3
+ * ==================================
4
+ *
5
+ * Detects instruction injection in AI agent memory and context files.
6
+ *
7
+ * Memory poisoning occurs when an adversary implants false or malicious
8
+ * instructions into an agent's persistent storage — the agent "learns"
9
+ * the instruction and recalls it in future sessions.
10
+ *
11
+ * Targets:
12
+ * - Claude Code memory files (.claude/memory/*.md, CLAUDE.md)
13
+ * - Cursor rules (.cursorrules, .cursor/rules/*.mdc)
14
+ * - Continue config (.continue/config.json, .continue/rules/*.md)
15
+ * - Windsurf rules (.windsurfrules)
16
+ * - Cody config (.cody/)
17
+ * - Gemini CLI (.gemini/)
18
+ * - Project docs that agents ingest (README, CONTRIBUTING, docs/)
19
+ *
20
+ * Attack vectors detected:
21
+ * 1. Direct instruction injection — "ignore previous instructions"
22
+ * 2. Hidden directives in markdown — invisible chars, HTML comments
23
+ * 3. Exfiltration instructions — "send", "upload", "POST to"
24
+ * 4. Tool abuse instructions — "run bash", "write to", "delete"
25
+ * 5. Persona hijacking — "you are now", "your new role"
26
+ * 6. Memory persistence — instructions designed to survive context resets
27
+ *
28
+ * Maps to: OWASP Agentic ASI01 (Agent Goal Hijacking),
29
+ * ASI05 (Memory/Context Poisoning)
30
+ */
31
+
32
+ import path from 'path';
33
+ import fg from 'fast-glob';
34
+ import { BaseAgent, createFinding } from './base-agent.js';
35
+
36
+ // =============================================================================
37
+ // MEMORY / CONTEXT FILES TO SCAN
38
+ // =============================================================================
39
+
40
+ const MEMORY_GLOBS = [
41
+ // Claude Code
42
+ '.claude/memory/*.md',
43
+ '.claude/commands/*.md',
44
+ 'CLAUDE.md',
45
+ // Cursor
46
+ '.cursorrules',
47
+ '.cursor/rules/*.mdc',
48
+ '.cursor/rules/*.md',
49
+ // Windsurf
50
+ '.windsurfrules',
51
+ // Continue
52
+ '.continue/config.json',
53
+ '.continue/rules/*.md',
54
+ // Cody
55
+ '.cody/*.md',
56
+ '.cody/config.json',
57
+ // Gemini CLI
58
+ '.gemini/*.md',
59
+ '.gemini/settings.json',
60
+ // Copilot
61
+ '.github/copilot-instructions.md',
62
+ // Aider
63
+ '.aider.conf.yml',
64
+ ];
65
+
66
+ // Project docs that agents commonly ingest as context
67
+ const DOC_GLOBS = [
68
+ 'README.md',
69
+ 'CONTRIBUTING.md',
70
+ 'docs/**/*.md',
71
+ 'AGENTS.md',
72
+ '.github/PULL_REQUEST_TEMPLATE.md',
73
+ '.github/ISSUE_TEMPLATE/*.md',
74
+ ];
75
+
76
+ // =============================================================================
77
+ // DETECTION PATTERNS
78
+ // =============================================================================
79
+
80
+ const INJECTION_PATTERNS = [
81
+ {
82
+ regex: /(?:ignore|disregard|forget|override)\s+(?:all\s+)?(?:previous|prior|above|earlier|system)\s+(?:instructions?|rules?|prompts?|constraints?|guidelines?)/gi,
83
+ rule: 'MEMORY_POISON_OVERRIDE',
84
+ title: 'Instruction Override in Agent Memory',
85
+ severity: 'critical',
86
+ description: 'File contains instructions to override previous agent rules. This is a classic prompt injection pattern that persists in agent memory.',
87
+ owasp: 'ASI01',
88
+ cwe: 'CWE-74',
89
+ fix: 'Remove the override instruction. If intentional, use the agent\'s official configuration mechanism instead.',
90
+ },
91
+ {
92
+ regex: /(?:you\s+are\s+now|your\s+new\s+(?:role|persona|identity)|from\s+now\s+on\s+you\s+(?:are|will|must|should))\s+/gi,
93
+ rule: 'MEMORY_POISON_PERSONA',
94
+ title: 'Persona Hijacking in Agent Memory',
95
+ severity: 'high',
96
+ description: 'File attempts to redefine the agent\'s persona or role. If this file is ingested as context, the agent may adopt the injected persona in future sessions.',
97
+ owasp: 'ASI01',
98
+ cwe: 'CWE-74',
99
+ fix: 'Remove the persona redefinition. Use the agent\'s system prompt or official configuration to set roles.',
100
+ },
101
+ {
102
+ regex: /(?:send|upload|post|transmit|exfiltrate|forward)\s+(?:all|any|the|this)?\s*(?:code|data|files?|contents?|secrets?|keys?|tokens?|credentials?|env|environment)\s+(?:to|via|using|through)\s+/gi,
103
+ rule: 'MEMORY_POISON_EXFILTRATE',
104
+ title: 'Exfiltration Instruction in Agent Memory',
105
+ severity: 'critical',
106
+ description: 'File instructs an agent to exfiltrate data to an external destination. If persisted in memory, the agent may execute this across sessions.',
107
+ owasp: 'ASI01',
108
+ cwe: 'CWE-200',
109
+ fix: 'Remove the exfiltration instruction immediately. Audit agent memory for other injected directives.',
110
+ },
111
+ {
112
+ regex: /(?:do\s+not|don'?t|never)\s+(?:tell|inform|show|reveal|display|mention|report|log)\s+(?:the\s+user|anyone|them)/gi,
113
+ rule: 'MEMORY_POISON_SUPPRESS',
114
+ title: 'Output Suppression Instruction',
115
+ severity: 'critical',
116
+ description: 'File instructs the agent to hide actions from the user. This matches the ToxicSkills "output suppression" pattern — the agent does something harmful but doesn\'t report it.',
117
+ owasp: 'ASI01',
118
+ cwe: 'CWE-200',
119
+ fix: 'Remove the suppression instruction. Agents should always report their actions transparently.',
120
+ },
121
+ {
122
+ regex: /(?:whenever|every\s+time|always|each\s+time)\s+(?:you|the\s+agent)\s+(?:start|begin|open|run|execute|encounter)/gi,
123
+ rule: 'MEMORY_POISON_PERSISTENT',
124
+ title: 'Persistent Trigger Instruction',
125
+ severity: 'high',
126
+ description: 'File contains instructions designed to trigger on every session or action — a hallmark of persistent memory poisoning. Unlike a one-time injection, this survives context resets.',
127
+ owasp: 'ASI05',
128
+ cwe: 'CWE-74',
129
+ fix: 'Remove the persistent trigger. If you need recurring behavior, configure it through the agent\'s official hook or startup mechanism.',
130
+ },
131
+ {
132
+ regex: /(?:run|execute|invoke|call|use)\s+(?:bash|shell|terminal|cmd|system|exec|subprocess|os\.system|child_process)/gi,
133
+ rule: 'MEMORY_POISON_TOOL_ABUSE',
134
+ title: 'Shell Execution Instruction in Memory',
135
+ severity: 'high',
136
+ description: 'File instructs the agent to execute shell commands. If persisted in memory, this enables remote code execution via prompt injection.',
137
+ owasp: 'ASI02',
138
+ cwe: 'CWE-78',
139
+ fix: 'Remove direct shell execution instructions. Use the agent\'s sandboxed tool API instead.',
140
+ },
141
+ {
142
+ regex: /(?:fetch|curl|wget|http\.get|axios\.get|request)\s*\(\s*['"`]https?:\/\//gi,
143
+ rule: 'MEMORY_POISON_NETWORK',
144
+ title: 'Network Request Instruction in Memory',
145
+ severity: 'high',
146
+ description: 'File instructs the agent to make network requests to hardcoded URLs. A poisoned memory file can use this to phone home or exfiltrate context.',
147
+ owasp: 'ASI01',
148
+ cwe: 'CWE-918',
149
+ fix: 'Remove the hardcoded network request. If the agent needs to fetch data, configure it through approved MCP servers or tools.',
150
+ },
151
+ ];
152
+
153
+ // Hidden content patterns (invisible chars, encoded payloads)
154
+ const HIDDEN_CONTENT_PATTERNS = [
155
+ {
156
+ // Unicode zero-width chars used to hide instructions
157
+ regex: /[\u200B\u200C\u200D\u2060\uFEFF]{3,}/g,
158
+ rule: 'MEMORY_HIDDEN_UNICODE',
159
+ title: 'Hidden Unicode Content in Agent File',
160
+ severity: 'critical',
161
+ description: 'File contains clusters of zero-width Unicode characters. These are invisible to humans but may encode hidden instructions that the agent processes.',
162
+ owasp: 'ASI01',
163
+ cwe: 'CWE-116',
164
+ fix: 'Strip all zero-width characters from this file. Use a hex editor to inspect for hidden content.',
165
+ },
166
+ {
167
+ // HTML comments in markdown that could contain injected instructions
168
+ regex: /<!--[\s\S]*?(?:ignore|override|system|role|always|execute|send\s+to|curl|bash)[\s\S]*?-->/gi,
169
+ rule: 'MEMORY_HIDDEN_COMMENT',
170
+ title: 'Suspicious HTML Comment in Agent File',
171
+ severity: 'high',
172
+ description: 'An HTML comment contains what appears to be an injected instruction. Some agents process HTML comments as context, making this a viable injection vector.',
173
+ owasp: 'ASI05',
174
+ cwe: 'CWE-74',
175
+ fix: 'Remove the suspicious HTML comment or move the content to a visible location.',
176
+ },
177
+ {
178
+ // Base64 encoded content in markdown/config files
179
+ regex: /(?:^|[\s"':=])([A-Za-z0-9+/]{60,}={0,2})(?:[\s"',]|$)/gm,
180
+ rule: 'MEMORY_HIDDEN_BASE64',
181
+ title: 'Base64-Encoded Content in Agent File',
182
+ severity: 'medium',
183
+ description: 'File contains a long base64-encoded string. This could hide instructions or payloads that the agent decodes and executes.',
184
+ owasp: 'ASI05',
185
+ cwe: 'CWE-116',
186
+ confidence: 'medium',
187
+ fix: 'Decode the base64 content and verify it is benign. Remove if it contains instructions or executable content.',
188
+ },
189
+ ];
190
+
191
+ // =============================================================================
192
+ // AGENT
193
+ // =============================================================================
194
+
195
+ export class MemoryPoisoningAgent extends BaseAgent {
196
+ constructor() {
197
+ super(
198
+ 'MemoryPoisoningAgent',
199
+ 'Detects instruction injection in AI agent memory and context files',
200
+ 'agentic'
201
+ );
202
+ }
203
+
204
+ shouldRun() {
205
+ return true; // Always run — memory poisoning applies to any project
206
+ }
207
+
208
+ async analyze(context) {
209
+ const { rootPath } = context;
210
+ const findings = [];
211
+
212
+ // Discover all memory/context files
213
+ const memoryFiles = await fg(MEMORY_GLOBS, {
214
+ cwd: rootPath,
215
+ absolute: true,
216
+ dot: true,
217
+ });
218
+
219
+ const docFiles = await fg(DOC_GLOBS, {
220
+ cwd: rootPath,
221
+ absolute: true,
222
+ dot: true,
223
+ });
224
+
225
+ // Scan memory files with higher severity (direct agent context)
226
+ for (const file of memoryFiles) {
227
+ findings.push(...this._scanFile(file, true));
228
+ }
229
+
230
+ // Scan doc files with slightly lower confidence (indirect context)
231
+ for (const file of docFiles) {
232
+ findings.push(...this._scanFile(file, false));
233
+ }
234
+
235
+ return findings;
236
+ }
237
+
238
+ _scanFile(filePath, isDirectMemory) {
239
+ const content = this.readFile(filePath);
240
+ if (!content) return [];
241
+
242
+ const findings = [];
243
+ const lines = content.split('\n');
244
+
245
+ // Check injection patterns
246
+ for (const pattern of INJECTION_PATTERNS) {
247
+ for (let i = 0; i < lines.length; i++) {
248
+ const line = lines[i];
249
+ if (this.isSuppressed(line)) continue;
250
+
251
+ pattern.regex.lastIndex = 0;
252
+ const match = pattern.regex.exec(line);
253
+ if (match) {
254
+ findings.push(createFinding({
255
+ file: filePath,
256
+ line: i + 1,
257
+ severity: pattern.severity,
258
+ category: 'agentic',
259
+ rule: pattern.rule,
260
+ title: pattern.title,
261
+ description: pattern.description + (isDirectMemory
262
+ ? ' This file is directly loaded into agent context.'
263
+ : ' This file may be ingested by agents as project context.'),
264
+ matched: match[0],
265
+ confidence: isDirectMemory ? 'high' : 'medium',
266
+ cwe: pattern.cwe,
267
+ owasp: pattern.owasp,
268
+ fix: pattern.fix,
269
+ }));
270
+ }
271
+ }
272
+ }
273
+
274
+ // Check hidden content patterns (full content, not per-line)
275
+ for (const pattern of HIDDEN_CONTENT_PATTERNS) {
276
+ pattern.regex.lastIndex = 0;
277
+ const match = pattern.regex.exec(content);
278
+ if (match) {
279
+ // Find approximate line number
280
+ const beforeMatch = content.slice(0, match.index);
281
+ const lineNum = (beforeMatch.match(/\n/g) || []).length + 1;
282
+
283
+ findings.push(createFinding({
284
+ file: filePath,
285
+ line: lineNum,
286
+ severity: pattern.severity,
287
+ category: 'agentic',
288
+ rule: pattern.rule,
289
+ title: pattern.title,
290
+ description: pattern.description,
291
+ matched: match[0].slice(0, 100),
292
+ confidence: pattern.confidence || (isDirectMemory ? 'high' : 'medium'),
293
+ cwe: pattern.cwe,
294
+ owasp: pattern.owasp,
295
+ fix: pattern.fix,
296
+ }));
297
+ }
298
+ }
299
+
300
+ return findings;
301
+ }
302
+ }
303
+
304
+ export default MemoryPoisoningAgent;