ship-safe 6.4.0 → 7.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.
- package/README.md +0 -2
- package/cli/agents/agent-config-scanner.js +15 -0
- package/cli/agents/deep-analyzer.js +39 -19
- package/cli/agents/index.js +4 -1
- package/cli/agents/memory-poisoning-agent.js +304 -0
- package/cli/agents/scoring-engine.js +16 -1
- package/cli/agents/supply-chain-agent.js +128 -2
- package/cli/bin/ship-safe.js +53 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/watch.js +205 -0
- package/cli/providers/llm-provider.js +89 -1
- package/cli/utils/compliance-map.js +66 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,11 +37,24 @@ const AGENT_RULES_FILES = [
|
|
|
37
37
|
'.github/copilot-instructions.md',
|
|
38
38
|
'.aider.conf.yml',
|
|
39
39
|
'.continue/config.json',
|
|
40
|
+
// Gemini CLI
|
|
41
|
+
'.gemini/settings.json',
|
|
42
|
+
'.gemini/rules.md',
|
|
43
|
+
// Cody (Sourcegraph)
|
|
44
|
+
'.cody/config.json',
|
|
45
|
+
'.cody/context.json',
|
|
46
|
+
// Augment Code
|
|
47
|
+
'.augment/config.json',
|
|
40
48
|
];
|
|
41
49
|
|
|
42
50
|
const AGENT_RULES_GLOBS = [
|
|
43
51
|
'.cursor/rules/*.mdc',
|
|
44
52
|
'.claude/commands/*.md',
|
|
53
|
+
// Gemini CLI commands
|
|
54
|
+
'.gemini/commands/*.md',
|
|
55
|
+
// Cody custom commands
|
|
56
|
+
'.cody/commands/*.md',
|
|
57
|
+
'.cody/rules/*.md',
|
|
45
58
|
];
|
|
46
59
|
|
|
47
60
|
const OPENCLAW_FILES = [
|
|
@@ -84,6 +97,8 @@ const MEMORY_GLOBS = [
|
|
|
84
97
|
'.claude/memory/**',
|
|
85
98
|
'.cursor/memory/**',
|
|
86
99
|
'.continue/memory/**',
|
|
100
|
+
'.gemini/memory/**',
|
|
101
|
+
'.cody/memory/**',
|
|
87
102
|
];
|
|
88
103
|
|
|
89
104
|
// =============================================================================
|
|
@@ -25,8 +25,15 @@ import { createProvider, autoDetectProvider } from '../providers/llm-provider.js
|
|
|
25
25
|
// CONSTANTS
|
|
26
26
|
// =============================================================================
|
|
27
27
|
|
|
28
|
-
/** Max file content
|
|
29
|
-
const
|
|
28
|
+
/** Max file content per finding for standard providers (tokens cost money) */
|
|
29
|
+
const MAX_FILE_CHARS_DEFAULT = 4000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Max file content per finding for large-context providers (Gemma 4 128K–256K).
|
|
33
|
+
* Sending the full file enables cross-function taint tracing that a 40-line
|
|
34
|
+
* window cannot catch.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_FILE_CHARS_LARGE_CTX = 80000;
|
|
30
37
|
|
|
31
38
|
/** Max findings to analyze per run (cost control) */
|
|
32
39
|
const MAX_FINDINGS = 30;
|
|
@@ -84,6 +91,14 @@ export class DeepAnalyzer {
|
|
|
84
91
|
this.verbose = options.verbose || false;
|
|
85
92
|
this.spentCents = 0;
|
|
86
93
|
this.analyzedCount = 0;
|
|
94
|
+
|
|
95
|
+
// If the provider advertises a large context window (Gemma 4, etc.),
|
|
96
|
+
// increase file context and batch size to take full advantage.
|
|
97
|
+
const ctxWindow = this.provider?.contextWindow ?? 0;
|
|
98
|
+
this.largeContext = ctxWindow >= 65536;
|
|
99
|
+
this.maxFileChars = this.largeContext ? MAX_FILE_CHARS_LARGE_CTX : MAX_FILE_CHARS_DEFAULT;
|
|
100
|
+
// Larger batches for local large-context models (no per-token cost)
|
|
101
|
+
this.batchSize = this.largeContext ? 15 : 5;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/**
|
|
@@ -91,11 +106,11 @@ export class DeepAnalyzer {
|
|
|
91
106
|
* Returns null if no provider is available.
|
|
92
107
|
*/
|
|
93
108
|
static create(rootPath, options = {}) {
|
|
94
|
-
// --local flag: use Ollama
|
|
109
|
+
// --local flag: use Gemma 4 via Ollama (structured output, large context)
|
|
95
110
|
if (options.local) {
|
|
96
|
-
const provider = createProvider('
|
|
97
|
-
model:
|
|
98
|
-
baseUrl: options.ollamaUrl
|
|
111
|
+
const provider = createProvider('gemma4', null, {
|
|
112
|
+
model: options.model,
|
|
113
|
+
baseUrl: options.ollamaUrl,
|
|
99
114
|
});
|
|
100
115
|
return new DeepAnalyzer({ provider, ...options });
|
|
101
116
|
}
|
|
@@ -141,11 +156,10 @@ export class DeepAnalyzer {
|
|
|
141
156
|
toAnalyze.length = Math.max(1, affordable);
|
|
142
157
|
}
|
|
143
158
|
|
|
144
|
-
// Batch findings
|
|
145
|
-
const batchSize = 5;
|
|
159
|
+
// Batch findings — larger batches for large-context providers (Gemma 4 etc.)
|
|
146
160
|
const results = new Map();
|
|
147
161
|
|
|
148
|
-
for (let i = 0; i < toAnalyze.length; i += batchSize) {
|
|
162
|
+
for (let i = 0; i < toAnalyze.length; i += this.batchSize) {
|
|
149
163
|
// Budget check before each batch
|
|
150
164
|
if (this.spentCents >= this.budgetCents) {
|
|
151
165
|
if (this.verbose) {
|
|
@@ -154,7 +168,7 @@ export class DeepAnalyzer {
|
|
|
154
168
|
break;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
const batch = toAnalyze.slice(i, i + batchSize);
|
|
171
|
+
const batch = toAnalyze.slice(i, i + this.batchSize);
|
|
158
172
|
const prompt = this._buildPrompt(batch, context);
|
|
159
173
|
|
|
160
174
|
try {
|
|
@@ -261,16 +275,22 @@ ${JSON.stringify(items, null, 2)}`;
|
|
|
261
275
|
const lines = content.split('\n');
|
|
262
276
|
const lineNum = finding.line || 1;
|
|
263
277
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.map((l, i) => `${
|
|
269
|
-
|
|
278
|
+
let context;
|
|
279
|
+
if (this.largeContext) {
|
|
280
|
+
// Large-context providers (Gemma 4): send the entire file so the model
|
|
281
|
+
// can trace taint flows across functions, not just the immediate window.
|
|
282
|
+
context = lines.map((l, i) => `${i + 1}: ${l}`).join('\n');
|
|
283
|
+
} else {
|
|
284
|
+
// Standard providers: 40-line window around the finding
|
|
285
|
+
const start = Math.max(0, lineNum - 21);
|
|
286
|
+
const end = Math.min(lines.length, lineNum + 20);
|
|
287
|
+
context = lines.slice(start, end)
|
|
288
|
+
.map((l, i) => `${start + i + 1}: ${l}`)
|
|
289
|
+
.join('\n');
|
|
290
|
+
}
|
|
270
291
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
context = context.slice(0, MAX_FILE_CHARS) + '\n... (truncated)';
|
|
292
|
+
if (context.length > this.maxFileChars) {
|
|
293
|
+
context = context.slice(0, this.maxFileChars) + '\n... (truncated)';
|
|
274
294
|
}
|
|
275
295
|
|
|
276
296
|
return context;
|
package/cli/agents/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export { PIIComplianceAgent } from './pii-compliance-agent.js';
|
|
|
26
26
|
export { VibeCodingAgent } from './vibe-coding-agent.js';
|
|
27
27
|
export { ExceptionHandlerAgent } from './exception-handler-agent.js';
|
|
28
28
|
export { AgentConfigScanner } from './agent-config-scanner.js';
|
|
29
|
+
export { MemoryPoisoningAgent } from './memory-poisoning-agent.js';
|
|
29
30
|
export { LegalRiskAgent, LEGALLY_RISKY_PACKAGES } from './legal-risk-agent.js';
|
|
30
31
|
export { ABOMGenerator } from './abom-generator.js';
|
|
31
32
|
export { VerifierAgent } from './verifier-agent.js';
|
|
@@ -36,7 +37,7 @@ export { PolicyEngine } from './policy-engine.js';
|
|
|
36
37
|
export { HTMLReporter } from './html-reporter.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Create a fully configured orchestrator with all
|
|
40
|
+
* Create a fully configured orchestrator with all 19 scanning agents.
|
|
40
41
|
* (VerifierAgent and DeepAnalyzer run as post-processors, not in the agent pool.)
|
|
41
42
|
*/
|
|
42
43
|
import { Orchestrator as OrchestratorClass } from './orchestrator.js';
|
|
@@ -58,6 +59,7 @@ import { PIIComplianceAgent as PIIComplianceAgentClass } from './pii-compliance-
|
|
|
58
59
|
import { VibeCodingAgent as VibeCodingAgentClass } from './vibe-coding-agent.js';
|
|
59
60
|
import { ExceptionHandlerAgent as ExceptionHandlerAgentClass } from './exception-handler-agent.js';
|
|
60
61
|
import { AgentConfigScanner as AgentConfigScannerClass } from './agent-config-scanner.js';
|
|
62
|
+
import { MemoryPoisoningAgent as MemoryPoisoningAgentClass } from './memory-poisoning-agent.js';
|
|
61
63
|
|
|
62
64
|
export function buildOrchestrator() {
|
|
63
65
|
const orchestrator = new OrchestratorClass();
|
|
@@ -80,6 +82,7 @@ export function buildOrchestrator() {
|
|
|
80
82
|
new VibeCodingAgentClass(),
|
|
81
83
|
new ExceptionHandlerAgentClass(),
|
|
82
84
|
new AgentConfigScannerClass(),
|
|
85
|
+
new MemoryPoisoningAgentClass(),
|
|
83
86
|
]);
|
|
84
87
|
return orchestrator;
|
|
85
88
|
}
|
|
@@ -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;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
|
-
import { getComplianceSummary } from '../utils/compliance-map.js';
|
|
14
|
+
import { getComplianceSummary, getAgenticSummary, enrichAgenticRisk } from '../utils/compliance-map.js';
|
|
15
15
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
// SCORING CONFIGURATION
|
|
@@ -49,6 +49,7 @@ const FALLBACK_CATEGORY_MAP = {
|
|
|
49
49
|
'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
|
|
50
50
|
'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
|
|
51
51
|
'agent-config': 'llm', // Agent config security → AI/LLM category
|
|
52
|
+
'memory-poisoning': 'llm', // Memory poisoning → AI/LLM category
|
|
52
53
|
'recon': null, // skip recon findings
|
|
53
54
|
};
|
|
54
55
|
|
|
@@ -87,6 +88,11 @@ export class ScoringEngine {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// ── Enrich findings with OWASP Agentic AI Top 10 metadata ──────────────
|
|
92
|
+
for (const finding of findings) {
|
|
93
|
+
enrichAgenticRisk(finding);
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
// ── Classify findings into categories ─────────────────────────────────────
|
|
91
97
|
for (const finding of findings) {
|
|
92
98
|
const cat = this.resolveCategory(finding.category);
|
|
@@ -146,6 +152,14 @@ export class ScoringEngine {
|
|
|
146
152
|
compliance = null;
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
// ── OWASP Agentic AI Top 10 summary ──────────────────────────────────
|
|
156
|
+
let agenticSummary;
|
|
157
|
+
try {
|
|
158
|
+
agenticSummary = getAgenticSummary(findings);
|
|
159
|
+
} catch {
|
|
160
|
+
agenticSummary = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
149
163
|
return {
|
|
150
164
|
score,
|
|
151
165
|
grade,
|
|
@@ -153,6 +167,7 @@ export class ScoringEngine {
|
|
|
153
167
|
totalFindings: findings.length,
|
|
154
168
|
totalDepVulns: depVulns.length,
|
|
155
169
|
compliance,
|
|
170
|
+
agenticSummary,
|
|
156
171
|
};
|
|
157
172
|
}
|
|
158
173
|
|
|
@@ -582,7 +582,8 @@ export class SupplyChainAudit extends BaseAgent {
|
|
|
582
582
|
}));
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
// ── 9.
|
|
585
|
+
// ── 9. Trojanized package behavioral signatures ───────────────────────────
|
|
586
|
+
// Patterns from Axios 1.8.2, LiteLLM 1.82.7, TeamPCP campaign (Mar 2026)
|
|
586
587
|
if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
|
|
587
588
|
try {
|
|
588
589
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -590,7 +591,132 @@ export class SupplyChainAudit extends BaseAgent {
|
|
|
590
591
|
...(pkg.dependencies || {}),
|
|
591
592
|
...(pkg.devDependencies || {}),
|
|
592
593
|
};
|
|
593
|
-
|
|
594
|
+
|
|
595
|
+
for (const depName of Object.keys(allDeps).slice(0, 80)) {
|
|
596
|
+
const depDir = path.join(rootPath, 'node_modules', depName);
|
|
597
|
+
const depPkgPath = path.join(depDir, 'package.json');
|
|
598
|
+
if (!fs.existsSync(depPkgPath)) continue;
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
602
|
+
|
|
603
|
+
// 9a. Hidden dependencies added by attacker
|
|
604
|
+
// Axios 1.8.2 injected a hidden malicious dep that wasn't in the legitimate version.
|
|
605
|
+
// Check for deps that only exist in certain version ranges.
|
|
606
|
+
const depDeps = depPkg.dependencies || {};
|
|
607
|
+
for (const [subDep, subVer] of Object.entries(depDeps)) {
|
|
608
|
+
// Packages with very high download counts that suddenly gain unknown subdependencies
|
|
609
|
+
if (/^[a-z]+-[a-z0-9]+-[a-z0-9]+$/.test(subDep) && typeof subVer === 'string' && subVer.startsWith('git+')) {
|
|
610
|
+
findings.push(createFinding({
|
|
611
|
+
file: depPkgPath,
|
|
612
|
+
line: 0,
|
|
613
|
+
severity: 'critical',
|
|
614
|
+
category: 'supply-chain',
|
|
615
|
+
rule: 'TROJAN_HIDDEN_DEP',
|
|
616
|
+
title: `Suspicious Hidden Dependency in ${depName}: ${subDep}`,
|
|
617
|
+
description: `"${depName}" depends on "${subDep}" via a git URL. This matches the Axios/TeamPCP trojanization pattern where attackers inject a malicious dependency into a popular package.`,
|
|
618
|
+
matched: `${subDep}: ${subVer}`,
|
|
619
|
+
fix: `Compare this version's dependencies against the official release. If "${subDep}" was not in the previous version, this package may be trojanized.`,
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 9b. Install scripts that read and exfiltrate env vars
|
|
625
|
+
// LiteLLM 1.82.7 harvested AWS/GCP/Azure tokens + SSH keys
|
|
626
|
+
const scripts = depPkg.scripts || {};
|
|
627
|
+
for (const hook of ['preinstall', 'install', 'postinstall']) {
|
|
628
|
+
const cmd = scripts[hook];
|
|
629
|
+
if (!cmd) continue;
|
|
630
|
+
|
|
631
|
+
// Environment variable harvesting
|
|
632
|
+
if (/process\.env|os\.environ|ENV\[|getenv/i.test(cmd) &&
|
|
633
|
+
/https?:|fetch|request|axios|curl|wget|net\./i.test(cmd)) {
|
|
634
|
+
findings.push(createFinding({
|
|
635
|
+
file: depPkgPath,
|
|
636
|
+
line: 0,
|
|
637
|
+
severity: 'critical',
|
|
638
|
+
category: 'supply-chain',
|
|
639
|
+
rule: 'TROJAN_ENV_EXFIL',
|
|
640
|
+
title: `Credential Harvesting in ${hook}: ${depName}`,
|
|
641
|
+
description: `"${depName}" reads environment variables and makes network requests during ${hook}. This is the exact pattern used in the LiteLLM/TeamPCP attack to steal cloud credentials.`,
|
|
642
|
+
matched: cmd.slice(0, 200),
|
|
643
|
+
fix: 'Remove this package immediately. Rotate any credentials (AWS, GCP, Azure tokens, SSH keys) that may have been exfiltrated.',
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// SSH key / credential file access in install scripts
|
|
648
|
+
if (/\.ssh|\.aws|\.azure|\.gcp|\.kube|\.docker|credentials|\.npmrc|\.pypirc/i.test(cmd)) {
|
|
649
|
+
findings.push(createFinding({
|
|
650
|
+
file: depPkgPath,
|
|
651
|
+
line: 0,
|
|
652
|
+
severity: 'critical',
|
|
653
|
+
category: 'supply-chain',
|
|
654
|
+
rule: 'TROJAN_CREDENTIAL_ACCESS',
|
|
655
|
+
title: `Credential File Access in ${hook}: ${depName}`,
|
|
656
|
+
description: `"${depName}" accesses credential files (.ssh, .aws, .kube, etc.) during ${hook}. This matches the TeamPCP credential theft pattern.`,
|
|
657
|
+
matched: cmd.slice(0, 200),
|
|
658
|
+
fix: 'Remove this package immediately and rotate all credentials in the accessed directories.',
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 9c. Scan actual JS entry files for runtime exfiltration patterns
|
|
664
|
+
const main = depPkg.main || 'index.js';
|
|
665
|
+
const entryPath = path.join(depDir, main);
|
|
666
|
+
if (fs.existsSync(entryPath)) {
|
|
667
|
+
try {
|
|
668
|
+
const entryContent = fs.readFileSync(entryPath, 'utf-8');
|
|
669
|
+
if (entryContent.length < 500_000) {
|
|
670
|
+
// DNS-based exfiltration (encode data in subdomain)
|
|
671
|
+
if (/dns\.resolve|dns\.lookup/i.test(entryContent) &&
|
|
672
|
+
/process\.env|os\.hostname/i.test(entryContent)) {
|
|
673
|
+
findings.push(createFinding({
|
|
674
|
+
file: entryPath,
|
|
675
|
+
line: 1,
|
|
676
|
+
severity: 'high',
|
|
677
|
+
category: 'supply-chain',
|
|
678
|
+
rule: 'TROJAN_DNS_EXFIL',
|
|
679
|
+
title: `DNS Exfiltration Pattern: ${depName}`,
|
|
680
|
+
description: `"${depName}" combines DNS lookups with system/env data reads — a known technique for exfiltrating data via DNS subdomains to bypass firewalls.`,
|
|
681
|
+
matched: 'dns.resolve + process.env',
|
|
682
|
+
confidence: 'medium',
|
|
683
|
+
fix: 'Inspect the DNS usage. Legitimate packages rarely combine DNS with environment variable reading.',
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// WebSocket-based C2
|
|
688
|
+
if (/new\s+WebSocket/i.test(entryContent) &&
|
|
689
|
+
/process\.env|child_process|exec/i.test(entryContent)) {
|
|
690
|
+
findings.push(createFinding({
|
|
691
|
+
file: entryPath,
|
|
692
|
+
line: 1,
|
|
693
|
+
severity: 'critical',
|
|
694
|
+
category: 'supply-chain',
|
|
695
|
+
rule: 'TROJAN_WEBSOCKET_C2',
|
|
696
|
+
title: `WebSocket C2 Pattern: ${depName}`,
|
|
697
|
+
description: `"${depName}" opens WebSocket connections combined with system command execution — consistent with a Remote Access Trojan.`,
|
|
698
|
+
matched: 'WebSocket + child_process',
|
|
699
|
+
fix: 'Remove this package immediately. Scan for persistence mechanisms (cron jobs, startup scripts).',
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} catch { /* skip */ }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
} catch { /* skip */ }
|
|
707
|
+
}
|
|
708
|
+
} catch { /* skip */ }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── 10. Blockchain C2 indicators (CanisterWorm / ICP) ────────────────────
|
|
712
|
+
if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
|
|
713
|
+
try {
|
|
714
|
+
const pkg2 = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
715
|
+
const allDeps2 = {
|
|
716
|
+
...(pkg2.dependencies || {}),
|
|
717
|
+
...(pkg2.devDependencies || {}),
|
|
718
|
+
};
|
|
719
|
+
for (const depName of Object.keys(allDeps2)) {
|
|
594
720
|
const depPkgPath = path.join(rootPath, 'node_modules', depName, 'package.json');
|
|
595
721
|
if (!fs.existsSync(depPkgPath)) continue;
|
|
596
722
|
try {
|