@safetnsr/vet 1.0.0 → 1.2.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/dist/checks/deps.js +6 -4
- package/dist/checks/integrity.js +65 -13
- package/dist/checks/memory.d.ts +2 -0
- package/dist/checks/memory.js +275 -0
- package/dist/checks/models.js +5 -0
- package/dist/checks/owasp-checks.d.ts +51 -0
- package/dist/checks/owasp-checks.js +670 -0
- package/dist/checks/owasp.js +2 -739
- package/dist/checks/scan.js +5 -33
- package/dist/checks/verify.d.ts +2 -0
- package/dist/checks/verify.js +219 -0
- package/dist/cli.js +7 -1
- package/dist/reporter.js +9 -5
- package/dist/util.d.ts +8 -1
- package/dist/util.js +40 -9
- package/package.json +1 -1
package/dist/checks/owasp.js
CHANGED
|
@@ -1,741 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
|
-
// ── Agent config file targets ─────────────────────────────────────────────────
|
|
4
|
-
const AGENT_CONFIG_TARGETS = [
|
|
5
|
-
'.claude', // dir: settings.json, commands/, etc.
|
|
6
|
-
'CLAUDE.md',
|
|
7
|
-
'AGENTS.md',
|
|
8
|
-
'.cursorrules',
|
|
9
|
-
'.cursor', // dir: settings.json, mcp.json
|
|
10
|
-
'.github/copilot-instructions.md',
|
|
11
|
-
'.mcp', // dir
|
|
12
|
-
'mcp.json',
|
|
13
|
-
'.aider.conf.yml',
|
|
14
|
-
'.continue', // dir
|
|
15
|
-
'.roomodes',
|
|
16
|
-
'.roo', // dir
|
|
17
|
-
'codex.md',
|
|
18
|
-
];
|
|
19
|
-
// Files that are specifically "instruction/config" text (not just any file)
|
|
20
|
-
const MCP_CONFIG_PATHS = [
|
|
21
|
-
'mcp.json',
|
|
22
|
-
'.mcp',
|
|
23
|
-
'.cursor/mcp.json',
|
|
24
|
-
'.claude/mcp.json',
|
|
25
|
-
];
|
|
26
|
-
// ── File helpers ──────────────────────────────────────────────────────────────
|
|
27
|
-
function isTextFile(filePath) {
|
|
28
|
-
try {
|
|
29
|
-
const buf = readFileSync(filePath);
|
|
30
|
-
const sampleSize = Math.min(512, buf.length);
|
|
31
|
-
for (let i = 0; i < sampleSize; i++) {
|
|
32
|
-
if (buf[i] === 0)
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function collectDirFiles(dir) {
|
|
42
|
-
const files = [];
|
|
43
|
-
try {
|
|
44
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
45
|
-
for (const entry of entries) {
|
|
46
|
-
const full = join(dir, entry.name);
|
|
47
|
-
if (entry.isFile()) {
|
|
48
|
-
files.push(full);
|
|
49
|
-
}
|
|
50
|
-
else if (entry.isDirectory()) {
|
|
51
|
-
files.push(...collectDirFiles(full));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch { /* skip */ }
|
|
56
|
-
return files;
|
|
57
|
-
}
|
|
58
|
-
function collectAgentConfigFiles(cwd) {
|
|
59
|
-
const files = [];
|
|
60
|
-
for (const target of AGENT_CONFIG_TARGETS) {
|
|
61
|
-
const full = join(cwd, target);
|
|
62
|
-
if (!existsSync(full))
|
|
63
|
-
continue;
|
|
64
|
-
try {
|
|
65
|
-
const s = statSync(full);
|
|
66
|
-
if (s.isFile()) {
|
|
67
|
-
files.push(full);
|
|
68
|
-
}
|
|
69
|
-
else if (s.isDirectory()) {
|
|
70
|
-
files.push(...collectDirFiles(full));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch { /* skip */ }
|
|
74
|
-
}
|
|
75
|
-
return [...new Set(files)];
|
|
76
|
-
}
|
|
77
|
-
function collectMcpConfigFiles(cwd) {
|
|
78
|
-
const files = [];
|
|
79
|
-
for (const target of MCP_CONFIG_PATHS) {
|
|
80
|
-
const full = join(cwd, target);
|
|
81
|
-
if (!existsSync(full))
|
|
82
|
-
continue;
|
|
83
|
-
try {
|
|
84
|
-
const s = statSync(full);
|
|
85
|
-
if (s.isFile()) {
|
|
86
|
-
files.push(full);
|
|
87
|
-
}
|
|
88
|
-
else if (s.isDirectory()) {
|
|
89
|
-
files.push(...collectDirFiles(full));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch { /* skip */ }
|
|
93
|
-
}
|
|
94
|
-
return [...new Set(files)];
|
|
95
|
-
}
|
|
96
|
-
function readTextFile(filePath) {
|
|
97
|
-
if (!isTextFile(filePath))
|
|
98
|
-
return null;
|
|
99
|
-
try {
|
|
100
|
-
return readFileSync(filePath, 'utf-8');
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// ── ASI01 — Agent Goal Hijack (prompt injection) ──────────────────────────────
|
|
107
|
-
function checkASI01(cwd, configFiles) {
|
|
108
|
-
const findings = [];
|
|
109
|
-
if (configFiles.length === 0)
|
|
110
|
-
return { findings, deduction: 0 };
|
|
111
|
-
const injectionKeywords = ['untrusted', 'injection', 'sanitize', 'validate input', 'input boundary', 'prompt injection', 'adversarial'];
|
|
112
|
-
const urlFetchPatterns = /(?:fetch|curl|wget|http\.get|axios\.get|request\.get)\s*\(/i;
|
|
113
|
-
const sanitizationKeywords = ['sanitize', 'escape', 'encode', 'validate', 'strip', 'clean'];
|
|
114
|
-
let injectionAwarenessFound = false;
|
|
115
|
-
for (const filePath of configFiles) {
|
|
116
|
-
const content = readTextFile(filePath);
|
|
117
|
-
if (!content)
|
|
118
|
-
continue;
|
|
119
|
-
const contentLower = content.toLowerCase();
|
|
120
|
-
if (injectionKeywords.some(kw => contentLower.includes(kw))) {
|
|
121
|
-
injectionAwarenessFound = true;
|
|
122
|
-
}
|
|
123
|
-
// Check for raw URL fetch instructions without sanitization guidance
|
|
124
|
-
const lines = content.split('\n');
|
|
125
|
-
for (let i = 0; i < lines.length; i++) {
|
|
126
|
-
const line = lines[i];
|
|
127
|
-
if (urlFetchPatterns.test(line)) {
|
|
128
|
-
// Check surrounding context (5 lines) for sanitization
|
|
129
|
-
const context = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 6)).join('\n').toLowerCase();
|
|
130
|
-
const hasSanitization = sanitizationKeywords.some(kw => context.includes(kw));
|
|
131
|
-
if (!hasSanitization) {
|
|
132
|
-
findings.push({
|
|
133
|
-
asiId: 'ASI01',
|
|
134
|
-
severity: 'warning',
|
|
135
|
-
message: 'ASI01: URL fetch instruction without sanitization guidance',
|
|
136
|
-
file: relative(cwd, filePath),
|
|
137
|
-
line: i + 1,
|
|
138
|
-
fixHint: 'add guidance to sanitize/validate content fetched from external URLs',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!injectionAwarenessFound) {
|
|
145
|
-
findings.push({
|
|
146
|
-
asiId: 'ASI01',
|
|
147
|
-
severity: 'warning',
|
|
148
|
-
message: 'ASI01: no prompt injection awareness — agent configs do not mention input validation or untrusted content handling',
|
|
149
|
-
fixHint: 'add instructions about handling untrusted input and prompt injection risks to your agent config',
|
|
150
|
-
});
|
|
151
|
-
return { findings, deduction: 15 };
|
|
152
|
-
}
|
|
153
|
-
return { findings, deduction: 0 };
|
|
154
|
-
}
|
|
155
|
-
// ── ASI02 — Tool Misuse and Exploitation ──────────────────────────────────────
|
|
156
|
-
function checkASI02(cwd, mcpFiles) {
|
|
157
|
-
const findings = [];
|
|
158
|
-
let deduction = 0;
|
|
159
|
-
for (const filePath of mcpFiles) {
|
|
160
|
-
const content = readTextFile(filePath);
|
|
161
|
-
if (!content)
|
|
162
|
-
continue;
|
|
163
|
-
let parsed;
|
|
164
|
-
try {
|
|
165
|
-
parsed = JSON.parse(content);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
const relPath = relative(cwd, filePath);
|
|
171
|
-
// Check for tools/mcpServers entries
|
|
172
|
-
const mcpConfig = parsed;
|
|
173
|
-
const servers = (mcpConfig.mcpServers ?? mcpConfig.servers ?? {});
|
|
174
|
-
for (const [toolName, toolConfig] of Object.entries(servers)) {
|
|
175
|
-
const tool = toolConfig;
|
|
176
|
-
const hasPermissions = tool.permissions != null || tool.allowedPaths != null || tool.restrictions != null;
|
|
177
|
-
const hasEnv = tool.env != null;
|
|
178
|
-
const args = Array.isArray(tool.args) ? tool.args : [];
|
|
179
|
-
// Shell/exec tools without argument validation
|
|
180
|
-
const isShellLike = ['bash', 'sh', 'zsh', 'powershell', 'cmd', 'exec'].some(s => toolName.toLowerCase().includes(s) || args.some((a) => typeof a === 'string' && a.includes(s)));
|
|
181
|
-
if (isShellLike && !hasPermissions) {
|
|
182
|
-
findings.push({
|
|
183
|
-
asiId: 'ASI02',
|
|
184
|
-
severity: 'error',
|
|
185
|
-
message: `ASI02: shell/exec tool "${toolName}" has no permission restrictions`,
|
|
186
|
-
file: relPath,
|
|
187
|
-
fixHint: 'add allowedPaths or restrictions to scope tool access',
|
|
188
|
-
});
|
|
189
|
-
deduction = Math.min(deduction + 10, 30);
|
|
190
|
-
}
|
|
191
|
-
else if (!hasPermissions && !hasEnv) {
|
|
192
|
-
findings.push({
|
|
193
|
-
asiId: 'ASI02',
|
|
194
|
-
severity: 'warning',
|
|
195
|
-
message: `ASI02: MCP tool "${toolName}" has no permission restrictions or path scoping`,
|
|
196
|
-
file: relPath,
|
|
197
|
-
fixHint: 'scope MCP tools with allowedPaths, permissions, or restrictions fields',
|
|
198
|
-
});
|
|
199
|
-
deduction = Math.min(deduction + 10, 30);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Check allowedTools in settings files
|
|
203
|
-
if (typeof mcpConfig.allowedTools === 'string' && mcpConfig.allowedTools.includes('*')) {
|
|
204
|
-
findings.push({
|
|
205
|
-
asiId: 'ASI02',
|
|
206
|
-
severity: 'warning',
|
|
207
|
-
message: 'ASI02: allowedTools contains wildcard — overly broad tool access',
|
|
208
|
-
file: relPath,
|
|
209
|
-
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
210
|
-
});
|
|
211
|
-
deduction = Math.min(deduction + 10, 30);
|
|
212
|
-
}
|
|
213
|
-
if (Array.isArray(mcpConfig.allowedTools)) {
|
|
214
|
-
const hasWildcard = mcpConfig.allowedTools.some(t => t === '*' || t === '**');
|
|
215
|
-
if (hasWildcard) {
|
|
216
|
-
findings.push({
|
|
217
|
-
asiId: 'ASI02',
|
|
218
|
-
severity: 'warning',
|
|
219
|
-
message: 'ASI02: allowedTools contains wildcard — overly broad tool access',
|
|
220
|
-
file: relPath,
|
|
221
|
-
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
222
|
-
});
|
|
223
|
-
deduction = Math.min(deduction + 10, 30);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// Also check .claude/settings.json for allowedTools wildcards
|
|
228
|
-
const claudeSettings = join(cwd, '.claude', 'settings.json');
|
|
229
|
-
if (existsSync(claudeSettings)) {
|
|
230
|
-
const content = readTextFile(claudeSettings);
|
|
231
|
-
if (content) {
|
|
232
|
-
try {
|
|
233
|
-
const settings = JSON.parse(content);
|
|
234
|
-
if (Array.isArray(settings.allowedTools)) {
|
|
235
|
-
const tools = settings.allowedTools;
|
|
236
|
-
const hasWildcard = tools.some(t => t === '*' || t === '**' || (typeof t === 'string' && t.endsWith(':*')));
|
|
237
|
-
if (hasWildcard) {
|
|
238
|
-
findings.push({
|
|
239
|
-
asiId: 'ASI02',
|
|
240
|
-
severity: 'warning',
|
|
241
|
-
message: 'ASI02: .claude/settings.json allowedTools contains wildcard pattern',
|
|
242
|
-
file: '.claude/settings.json',
|
|
243
|
-
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
244
|
-
});
|
|
245
|
-
deduction = Math.min(deduction + 10, 30);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
catch { /* skip */ }
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return { findings, deduction };
|
|
253
|
-
}
|
|
254
|
-
// ── ASI03 — Identity and Privilege Abuse ──────────────────────────────────────
|
|
255
|
-
function checkASI03(cwd, configFiles) {
|
|
256
|
-
const findings = [];
|
|
257
|
-
let deduction = 0;
|
|
258
|
-
const credentialPatterns = [
|
|
259
|
-
{ pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*["']?[A-Za-z0-9\-_]{16,}/i, label: 'API key' },
|
|
260
|
-
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9\-_+/]{16,}/i, label: 'secret/token' },
|
|
261
|
-
{ pattern: /ssh-(?:rsa|ed25519|ecdsa)\s+[A-Za-z0-9+/]{20,}/i, label: 'SSH public key' },
|
|
262
|
-
{ pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i, label: 'private key' },
|
|
263
|
-
{ pattern: /(?:AWS|GOOGLE|AZURE)_[A-Z_]+\s*[=:]\s*["']?[A-Za-z0-9/+]{16,}/i, label: 'cloud credential' },
|
|
264
|
-
];
|
|
265
|
-
const sudoPattern = /\bsudo\b|\bsu\s+-\b|run as root|elevated privileges|administrator rights/i;
|
|
266
|
-
const leastPrivKeywords = ['least.?privilege', 'minimum.?permission', 'scoped credentials', 'credential scop', 'no credentials', 'avoid.*key', 'don.*t.*store.*key'];
|
|
267
|
-
let hasLeastPrivMention = false;
|
|
268
|
-
let hasEnvFileRef = false;
|
|
269
|
-
for (const filePath of configFiles) {
|
|
270
|
-
const content = readTextFile(filePath);
|
|
271
|
-
if (!content)
|
|
272
|
-
continue;
|
|
273
|
-
const contentLower = content.toLowerCase();
|
|
274
|
-
const relPath = relative(cwd, filePath);
|
|
275
|
-
// Check for least-privilege mentions
|
|
276
|
-
if (leastPrivKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
277
|
-
hasLeastPrivMention = true;
|
|
278
|
-
}
|
|
279
|
-
// Check for .env file references in instruction files (not in .env themselves)
|
|
280
|
-
if (!filePath.endsWith('.env') && !filePath.includes('.env.')) {
|
|
281
|
-
if (contentLower.includes('.env') && /load|source|read|require|import/i.test(content)) {
|
|
282
|
-
if (!hasEnvFileRef) {
|
|
283
|
-
findings.push({
|
|
284
|
-
asiId: 'ASI03',
|
|
285
|
-
severity: 'warning',
|
|
286
|
-
message: 'ASI03: agent config references .env file — credentials may be accessible to agent',
|
|
287
|
-
file: relPath,
|
|
288
|
-
fixHint: 'ensure .env is not exposed to agent scope; use secret managers or scoped env vars',
|
|
289
|
-
});
|
|
290
|
-
deduction = Math.min(deduction + 15, 30);
|
|
291
|
-
hasEnvFileRef = true;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Check for hardcoded credentials
|
|
296
|
-
const lines = content.split('\n');
|
|
297
|
-
for (let i = 0; i < lines.length; i++) {
|
|
298
|
-
const line = lines[i];
|
|
299
|
-
for (const { pattern, label } of credentialPatterns) {
|
|
300
|
-
if (pattern.test(line)) {
|
|
301
|
-
findings.push({
|
|
302
|
-
asiId: 'ASI03',
|
|
303
|
-
severity: 'error',
|
|
304
|
-
message: `ASI03: possible ${label} in agent config`,
|
|
305
|
-
file: relPath,
|
|
306
|
-
line: i + 1,
|
|
307
|
-
fixHint: 'remove credentials from agent configs; use environment variables or secret managers',
|
|
308
|
-
});
|
|
309
|
-
deduction = Math.min(deduction + 15, 30);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Sudo/root patterns
|
|
313
|
-
if (sudoPattern.test(line)) {
|
|
314
|
-
findings.push({
|
|
315
|
-
asiId: 'ASI03',
|
|
316
|
-
severity: 'warning',
|
|
317
|
-
message: 'ASI03: agent config grants sudo/root access — privilege escalation risk',
|
|
318
|
-
file: relPath,
|
|
319
|
-
line: i + 1,
|
|
320
|
-
fixHint: 'restrict agent to least-privilege — avoid sudo and root access in agent instructions',
|
|
321
|
-
});
|
|
322
|
-
deduction = Math.min(deduction + 15, 30);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (!hasLeastPrivMention && configFiles.length > 0) {
|
|
327
|
-
findings.push({
|
|
328
|
-
asiId: 'ASI03',
|
|
329
|
-
severity: 'info',
|
|
330
|
-
message: 'ASI03: no mention of least-privilege or credential scoping in agent configs',
|
|
331
|
-
fixHint: 'document credential access policies and least-privilege principles in agent config',
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
return { findings, deduction };
|
|
335
|
-
}
|
|
336
|
-
// ── ASI04 — Agentic Supply Chain Vulnerabilities ──────────────────────────────
|
|
337
|
-
function checkASI04(cwd, mcpFiles) {
|
|
338
|
-
const findings = [];
|
|
339
|
-
let deduction = 0;
|
|
340
|
-
const localhostPattern = /^(https?:\/\/)?localhost|^(https?:\/\/)?127\.|^(https?:\/\/)?0\.0\.0\.0/i;
|
|
341
|
-
for (const filePath of mcpFiles) {
|
|
342
|
-
const content = readTextFile(filePath);
|
|
343
|
-
if (!content)
|
|
344
|
-
continue;
|
|
345
|
-
let parsed;
|
|
346
|
-
try {
|
|
347
|
-
parsed = JSON.parse(content);
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const relPath = relative(cwd, filePath);
|
|
353
|
-
const mcpConfig = parsed;
|
|
354
|
-
const servers = (mcpConfig.mcpServers ?? mcpConfig.servers ?? {});
|
|
355
|
-
for (const [toolName, toolConfig] of Object.entries(servers)) {
|
|
356
|
-
const tool = toolConfig;
|
|
357
|
-
const url = typeof tool.url === 'string' ? tool.url : null;
|
|
358
|
-
const command = typeof tool.command === 'string' ? tool.command : null;
|
|
359
|
-
const version = typeof tool.version === 'string' ? tool.version : null;
|
|
360
|
-
// External URL MCP server
|
|
361
|
-
if (url && !localhostPattern.test(url)) {
|
|
362
|
-
findings.push({
|
|
363
|
-
asiId: 'ASI04',
|
|
364
|
-
severity: 'warning',
|
|
365
|
-
message: `ASI04: MCP server "${toolName}" points to external URL: ${url}`,
|
|
366
|
-
file: relPath,
|
|
367
|
-
fixHint: 'verify external MCP servers; prefer localhost/self-hosted; pin versions',
|
|
368
|
-
});
|
|
369
|
-
deduction += 10;
|
|
370
|
-
}
|
|
371
|
-
// Unpinned tool version
|
|
372
|
-
if (command && !version) {
|
|
373
|
-
// Check if npx command without pinned version
|
|
374
|
-
if (/npx\s+[^@\s]+(?!\s*@)/.test(command) || (command === 'npx' && !version)) {
|
|
375
|
-
const args = Array.isArray(tool.args) ? tool.args : [];
|
|
376
|
-
const firstArg = args[0];
|
|
377
|
-
if (typeof firstArg === 'string' && !firstArg.includes('@') && !firstArg.startsWith('-')) {
|
|
378
|
-
findings.push({
|
|
379
|
-
asiId: 'ASI04',
|
|
380
|
-
severity: 'warning',
|
|
381
|
-
message: `ASI04: MCP tool "${toolName}" uses unpinned npx package "${firstArg}"`,
|
|
382
|
-
file: relPath,
|
|
383
|
-
fixHint: 'pin package versions (e.g. @scope/package@1.2.3) to prevent supply chain attacks',
|
|
384
|
-
});
|
|
385
|
-
deduction += 10;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Check agent config files for unverified external references
|
|
392
|
-
const allConfigFiles = collectAgentConfigFiles(cwd);
|
|
393
|
-
const externalAgentPattern = /(?:install|use|run|execute)\s+(?:https?:\/\/[^\s"']+|npm\s+i(?:nstall)?\s+[^\s"']+)/i;
|
|
394
|
-
const unpinnedNpmPattern = /npm\s+i(?:nstall)?\s+(?:@[^/]+\/)?[a-z][a-z0-9\-]*(?!\s*@[0-9])/i;
|
|
395
|
-
for (const filePath of allConfigFiles) {
|
|
396
|
-
const content = readTextFile(filePath);
|
|
397
|
-
if (!content)
|
|
398
|
-
continue;
|
|
399
|
-
const relPath = relative(cwd, filePath);
|
|
400
|
-
const lines = content.split('\n');
|
|
401
|
-
for (let i = 0; i < lines.length; i++) {
|
|
402
|
-
const line = lines[i];
|
|
403
|
-
if (unpinnedNpmPattern.test(line) && !/pinned|verified|trusted/i.test(line)) {
|
|
404
|
-
findings.push({
|
|
405
|
-
asiId: 'ASI04',
|
|
406
|
-
severity: 'info',
|
|
407
|
-
message: 'ASI04: agent config references npm package without pinned version',
|
|
408
|
-
file: relPath,
|
|
409
|
-
line: i + 1,
|
|
410
|
-
fixHint: 'pin npm package versions in agent instructions to prevent supply chain attacks',
|
|
411
|
-
});
|
|
412
|
-
deduction += 10;
|
|
413
|
-
break; // One per file
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return { findings, deduction };
|
|
418
|
-
}
|
|
419
|
-
// ── ASI05 — Unexpected Code Execution ────────────────────────────────────────
|
|
420
|
-
function checkASI05(cwd, configFiles) {
|
|
421
|
-
const findings = [];
|
|
422
|
-
if (configFiles.length === 0)
|
|
423
|
-
return { findings, deduction: 0 };
|
|
424
|
-
const unrestrictedExecPattern = /(?:allow|can|may|enabled?)\s+(?:to\s+)?(?:run|execute|exec|eval|shell)/i;
|
|
425
|
-
const sandboxKeywords = ['sandbox', 'container', 'docker', 'isolated', 'restricted', 'approval', 'review before', 'confirm before'];
|
|
426
|
-
const codeApprovalKeywords = ['review', 'approve', 'confirm', 'gate', 'human.?in.?the.?loop', 'ask before'];
|
|
427
|
-
let hasSandboxMention = false;
|
|
428
|
-
let hasCodeApproval = false;
|
|
429
|
-
let hasUnrestrictedExec = false;
|
|
430
|
-
for (const filePath of configFiles) {
|
|
431
|
-
const content = readTextFile(filePath);
|
|
432
|
-
if (!content)
|
|
433
|
-
continue;
|
|
434
|
-
const contentLower = content.toLowerCase();
|
|
435
|
-
if (sandboxKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
436
|
-
hasSandboxMention = true;
|
|
437
|
-
}
|
|
438
|
-
if (codeApprovalKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
439
|
-
hasCodeApproval = true;
|
|
440
|
-
}
|
|
441
|
-
// Check for autoApprove or unrestricted execution patterns
|
|
442
|
-
const lines = content.split('\n');
|
|
443
|
-
const relPath = relative(cwd, filePath);
|
|
444
|
-
for (let i = 0; i < lines.length; i++) {
|
|
445
|
-
const line = lines[i];
|
|
446
|
-
if (/autoApprove|auto.?approve/i.test(line) && !/false|disabled?|no\s+auto/i.test(line)) {
|
|
447
|
-
findings.push({
|
|
448
|
-
asiId: 'ASI05',
|
|
449
|
-
severity: 'warning',
|
|
450
|
-
message: 'ASI05: autoApprove pattern detected — code execution may proceed without human review',
|
|
451
|
-
file: relPath,
|
|
452
|
-
line: i + 1,
|
|
453
|
-
fixHint: 'require human approval gates before executing generated code',
|
|
454
|
-
});
|
|
455
|
-
hasUnrestrictedExec = true;
|
|
456
|
-
}
|
|
457
|
-
if (unrestrictedExecPattern.test(line) && !sandboxKeywords.some(kw => new RegExp(kw, 'i').test(line))) {
|
|
458
|
-
findings.push({
|
|
459
|
-
asiId: 'ASI05',
|
|
460
|
-
severity: 'info',
|
|
461
|
-
message: 'ASI05: agent config allows code execution — ensure sandbox/approval controls are in place',
|
|
462
|
-
file: relPath,
|
|
463
|
-
line: i + 1,
|
|
464
|
-
fixHint: 'add sandbox restrictions or approval gates for code execution',
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (hasUnrestrictedExec || (!hasSandboxMention && !hasCodeApproval)) {
|
|
470
|
-
// Check if there's actually any exec-related content before penalizing
|
|
471
|
-
const hasExecContent = configFiles.some(f => {
|
|
472
|
-
const c = readTextFile(f);
|
|
473
|
-
return c && /exec|shell|run|execute|eval|bash|python|node/i.test(c);
|
|
474
|
-
});
|
|
475
|
-
if (hasExecContent && !hasSandboxMention && !hasCodeApproval) {
|
|
476
|
-
findings.push({
|
|
477
|
-
asiId: 'ASI05',
|
|
478
|
-
severity: 'warning',
|
|
479
|
-
message: 'ASI05: agent config references code execution without sandbox or approval gates',
|
|
480
|
-
fixHint: 'document sandbox restrictions and require approval for code execution in agent configs',
|
|
481
|
-
});
|
|
482
|
-
return { findings, deduction: 10 };
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return { findings, deduction: hasUnrestrictedExec ? 10 : 0 };
|
|
486
|
-
}
|
|
487
|
-
// ── ASI06 — Memory and Context Poisoning ─────────────────────────────────────
|
|
488
|
-
function checkASI06(cwd) {
|
|
489
|
-
const findings = [];
|
|
490
|
-
let deduction = 0;
|
|
491
|
-
const memoryPaths = [
|
|
492
|
-
'.claude/memory',
|
|
493
|
-
'.cursor/memory',
|
|
494
|
-
'memory',
|
|
495
|
-
'.aider.chat.history.md',
|
|
496
|
-
'.continue/memory',
|
|
497
|
-
'agent-memory',
|
|
498
|
-
'context-store',
|
|
499
|
-
];
|
|
500
|
-
const gitignorePath = join(cwd, '.gitignore');
|
|
501
|
-
let gitignoreContent = '';
|
|
502
|
-
try {
|
|
503
|
-
gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
504
|
-
}
|
|
505
|
-
catch { /* no gitignore */ }
|
|
506
|
-
for (const memPath of memoryPaths) {
|
|
507
|
-
const full = join(cwd, memPath);
|
|
508
|
-
if (!existsSync(full))
|
|
509
|
-
continue;
|
|
510
|
-
const relPath = memPath;
|
|
511
|
-
// Check if memory files are tracked by git (not in .gitignore)
|
|
512
|
-
const isIgnored = gitignoreContent.includes(memPath) || gitignoreContent.includes(memPath.split('/')[0]);
|
|
513
|
-
if (!isIgnored) {
|
|
514
|
-
findings.push({
|
|
515
|
-
asiId: 'ASI06',
|
|
516
|
-
severity: 'warning',
|
|
517
|
-
message: `ASI06: agent memory path "${relPath}" is not in .gitignore — could be poisoned via PR`,
|
|
518
|
-
file: relPath,
|
|
519
|
-
fixHint: 'add agent memory directories to .gitignore to prevent context poisoning via PRs',
|
|
520
|
-
});
|
|
521
|
-
deduction += 8;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
// Check for RAG/embedding configs without filtering mentions
|
|
525
|
-
const ragPatterns = ['.continue/config.json', '.cursor/settings.json'];
|
|
526
|
-
for (const ragPath of ragPatterns) {
|
|
527
|
-
const full = join(cwd, ragPath);
|
|
528
|
-
if (!existsSync(full))
|
|
529
|
-
continue;
|
|
530
|
-
const content = readTextFile(full);
|
|
531
|
-
if (!content)
|
|
532
|
-
continue;
|
|
533
|
-
const hasRag = /embed|rag|retrieval|vector|index/i.test(content);
|
|
534
|
-
const hasFiltering = /filter|sanitize|validate|allowlist|blocklist/i.test(content);
|
|
535
|
-
if (hasRag && !hasFiltering) {
|
|
536
|
-
findings.push({
|
|
537
|
-
asiId: 'ASI06',
|
|
538
|
-
severity: 'warning',
|
|
539
|
-
message: `ASI06: RAG/embedding config "${ragPath}" has no input filtering`,
|
|
540
|
-
file: ragPath,
|
|
541
|
-
fixHint: 'add input filtering and validation for RAG/embedding sources to prevent context poisoning',
|
|
542
|
-
});
|
|
543
|
-
deduction += 8;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return { findings, deduction };
|
|
547
|
-
}
|
|
548
|
-
// ── ASI07 — Insecure Inter-Agent Communication ───────────────────────────────
|
|
549
|
-
function checkASI07(cwd, configFiles) {
|
|
550
|
-
const findings = [];
|
|
551
|
-
let deduction = 0;
|
|
552
|
-
const multiAgentKeywords = [
|
|
553
|
-
'a2a', 'agent-to-agent', 'multi.?agent', 'subagent', 'sub-agent',
|
|
554
|
-
'spawn agent', 'delegate', 'orchestrat', 'swarm', 'crew', 'autogen',
|
|
555
|
-
];
|
|
556
|
-
const authKeywords = ['auth', 'token', 'signed', 'hmac', 'jwt', 'api.?key', 'verify', 'authenticated'];
|
|
557
|
-
const encryptionKeywords = ['encrypt', 'tls', 'ssl', 'https', 'secure channel'];
|
|
558
|
-
let isMultiAgentSetup = false;
|
|
559
|
-
let hasAuthMention = false;
|
|
560
|
-
for (const filePath of configFiles) {
|
|
561
|
-
const content = readTextFile(filePath);
|
|
562
|
-
if (!content)
|
|
563
|
-
continue;
|
|
564
|
-
const contentLower = content.toLowerCase();
|
|
565
|
-
const hasMultiAgent = multiAgentKeywords.some(kw => new RegExp(kw, 'i').test(content));
|
|
566
|
-
if (hasMultiAgent) {
|
|
567
|
-
isMultiAgentSetup = true;
|
|
568
|
-
const relPath = relative(cwd, filePath);
|
|
569
|
-
if (authKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
570
|
-
hasAuthMention = true;
|
|
571
|
-
}
|
|
572
|
-
// Look for inter-agent communication patterns without auth
|
|
573
|
-
const lines = content.split('\n');
|
|
574
|
-
for (let i = 0; i < lines.length; i++) {
|
|
575
|
-
const line = lines[i];
|
|
576
|
-
const lineHasMultiAgent = multiAgentKeywords.some(kw => new RegExp(kw, 'i').test(line));
|
|
577
|
-
if (lineHasMultiAgent) {
|
|
578
|
-
const context = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join('\n');
|
|
579
|
-
const hasLocalAuth = authKeywords.some(kw => new RegExp(kw, 'i').test(context));
|
|
580
|
-
const hasLocalEncrypt = encryptionKeywords.some(kw => new RegExp(kw, 'i').test(context));
|
|
581
|
-
if (!hasLocalAuth && !hasLocalEncrypt) {
|
|
582
|
-
findings.push({
|
|
583
|
-
asiId: 'ASI07',
|
|
584
|
-
severity: 'warning',
|
|
585
|
-
message: 'ASI07: inter-agent communication pattern without authentication or encryption context',
|
|
586
|
-
file: relPath,
|
|
587
|
-
line: i + 1,
|
|
588
|
-
fixHint: 'ensure agent-to-agent channels use authentication tokens and encrypted transport',
|
|
589
|
-
});
|
|
590
|
-
deduction += 8;
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
// Only report if multi-agent setup is actually detected
|
|
598
|
-
if (isMultiAgentSetup && !hasAuthMention) {
|
|
599
|
-
if (!findings.some(f => f.asiId === 'ASI07')) {
|
|
600
|
-
findings.push({
|
|
601
|
-
asiId: 'ASI07',
|
|
602
|
-
severity: 'warning',
|
|
603
|
-
message: 'ASI07: multi-agent setup detected but no authentication mentioned for inter-agent communication',
|
|
604
|
-
fixHint: 'add authentication and authorization requirements for agent-to-agent communication',
|
|
605
|
-
});
|
|
606
|
-
deduction = Math.min(deduction + 8, 24);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return { findings, deduction };
|
|
610
|
-
}
|
|
611
|
-
// ── ASI08 — Cascading Failures ────────────────────────────────────────────────
|
|
612
|
-
function checkASI08(cwd, configFiles) {
|
|
613
|
-
const findings = [];
|
|
614
|
-
if (configFiles.length === 0)
|
|
615
|
-
return { findings, deduction: 0 };
|
|
616
|
-
const workflowKeywords = ['step', 'workflow', 'pipeline', 'sequence', 'chain', 'loop', 'iterate'];
|
|
617
|
-
const errorHandlingKeywords = [
|
|
618
|
-
'rollback', 'undo', 'revert', 'recover', 'retry', 'error handling', 'handle error',
|
|
619
|
-
'circuit breaker', 'rate limit', 'max retries', 'timeout', 'fail safe', 'fallback',
|
|
620
|
-
'on error', 'if it fails', 'if something goes wrong',
|
|
621
|
-
];
|
|
622
|
-
let hasWorkflowContent = false;
|
|
623
|
-
let hasErrorHandling = false;
|
|
624
|
-
for (const filePath of configFiles) {
|
|
625
|
-
const content = readTextFile(filePath);
|
|
626
|
-
if (!content)
|
|
627
|
-
continue;
|
|
628
|
-
if (workflowKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
629
|
-
hasWorkflowContent = true;
|
|
630
|
-
}
|
|
631
|
-
if (errorHandlingKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
632
|
-
hasErrorHandling = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (hasWorkflowContent && !hasErrorHandling) {
|
|
636
|
-
findings.push({
|
|
637
|
-
asiId: 'ASI08',
|
|
638
|
-
severity: 'warning',
|
|
639
|
-
message: 'ASI08: multi-step workflow config lacks error handling, rollback, or recovery instructions',
|
|
640
|
-
fixHint: 'add error handling instructions: rollback procedures, retry limits, and circuit breakers',
|
|
641
|
-
});
|
|
642
|
-
return { findings, deduction: 5 };
|
|
643
|
-
}
|
|
644
|
-
return { findings, deduction: 0 };
|
|
645
|
-
}
|
|
646
|
-
// ── ASI09 — Human-Agent Trust Exploitation ────────────────────────────────────
|
|
647
|
-
function checkASI09(cwd, configFiles) {
|
|
648
|
-
const findings = [];
|
|
649
|
-
if (configFiles.length === 0)
|
|
650
|
-
return { findings, deduction: 0 };
|
|
651
|
-
const destructiveKeywords = ['delete', 'drop', 'remove', 'deploy', 'publish', 'push', 'rm ', 'truncate'];
|
|
652
|
-
const approvalKeywords = [
|
|
653
|
-
'confirm', 'approval', 'approve', 'ask.*before', 'human.*review', 'manual.*review',
|
|
654
|
-
'permission', 'consent', 'verify.*before', 'check.*before', 'gate',
|
|
655
|
-
];
|
|
656
|
-
let hasDestructiveOps = false;
|
|
657
|
-
let hasApprovalGate = false;
|
|
658
|
-
for (const filePath of configFiles) {
|
|
659
|
-
const content = readTextFile(filePath);
|
|
660
|
-
if (!content)
|
|
661
|
-
continue;
|
|
662
|
-
if (destructiveKeywords.some(kw => new RegExp(`\\b${kw}\\b`, 'i').test(content))) {
|
|
663
|
-
hasDestructiveOps = true;
|
|
664
|
-
}
|
|
665
|
-
if (approvalKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
666
|
-
hasApprovalGate = true;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
// Check for autoApprove in settings
|
|
670
|
-
const claudeSettings = join(cwd, '.claude', 'settings.json');
|
|
671
|
-
if (existsSync(claudeSettings)) {
|
|
672
|
-
const content = readTextFile(claudeSettings);
|
|
673
|
-
if (content) {
|
|
674
|
-
try {
|
|
675
|
-
const settings = JSON.parse(content);
|
|
676
|
-
if (settings.autoApprove === true || (Array.isArray(settings.autoApprove) && settings.autoApprove.length > 0)) {
|
|
677
|
-
findings.push({
|
|
678
|
-
asiId: 'ASI09',
|
|
679
|
-
severity: 'warning',
|
|
680
|
-
message: 'ASI09: autoApprove enabled in .claude/settings.json — destructive ops may run unattended',
|
|
681
|
-
file: '.claude/settings.json',
|
|
682
|
-
fixHint: 'disable autoApprove or restrict it to non-destructive operations only',
|
|
683
|
-
});
|
|
684
|
-
return { findings, deduction: 10 };
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
catch { /* skip */ }
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (hasDestructiveOps && !hasApprovalGate) {
|
|
691
|
-
findings.push({
|
|
692
|
-
asiId: 'ASI09',
|
|
693
|
-
severity: 'warning',
|
|
694
|
-
message: 'ASI09: agent config references destructive operations (delete/deploy/publish) without approval gates',
|
|
695
|
-
fixHint: 'require explicit human confirmation before destructive operations (delete, deploy, publish)',
|
|
696
|
-
});
|
|
697
|
-
return { findings, deduction: 10 };
|
|
698
|
-
}
|
|
699
|
-
if (!hasApprovalGate) {
|
|
700
|
-
findings.push({
|
|
701
|
-
asiId: 'ASI09',
|
|
702
|
-
severity: 'info',
|
|
703
|
-
message: 'ASI09: no human approval gates mentioned in agent configs',
|
|
704
|
-
fixHint: 'document which operations require human approval in your agent config',
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
return { findings, deduction: 0 };
|
|
708
|
-
}
|
|
709
|
-
// ── ASI10 — Rogue Agents ─────────────────────────────────────────────────────
|
|
710
|
-
function checkASI10(cwd, configFiles) {
|
|
711
|
-
const findings = [];
|
|
712
|
-
if (configFiles.length === 0)
|
|
713
|
-
return { findings, deduction: 0 };
|
|
714
|
-
const monitoringKeywords = [
|
|
715
|
-
'log', 'audit', 'monitor', 'alert', 'trace', 'observ', 'kill switch',
|
|
716
|
-
'stop', 'timeout', 'session limit', 'max token', 'budget', 'governance',
|
|
717
|
-
];
|
|
718
|
-
let hasGovernance = false;
|
|
719
|
-
for (const filePath of configFiles) {
|
|
720
|
-
const content = readTextFile(filePath);
|
|
721
|
-
if (!content)
|
|
722
|
-
continue;
|
|
723
|
-
if (monitoringKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
724
|
-
hasGovernance = true;
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (!hasGovernance) {
|
|
729
|
-
findings.push({
|
|
730
|
-
asiId: 'ASI10',
|
|
731
|
-
severity: 'info',
|
|
732
|
-
message: 'ASI10: no monitoring, logging, or kill switch mechanisms mentioned in agent configs',
|
|
733
|
-
fixHint: 'add monitoring/audit trail requirements and session limits to your agent config',
|
|
734
|
-
});
|
|
735
|
-
return { findings, deduction: 5 };
|
|
736
|
-
}
|
|
737
|
-
return { findings, deduction: 0 };
|
|
738
|
-
}
|
|
1
|
+
import { collectAgentConfigFiles, collectMcpConfigFiles, checkASI01, checkASI02, checkASI03, checkASI04, checkASI05, checkASI06, checkASI07, checkASI08, checkASI09, checkASI10, } from './owasp-checks.js';
|
|
739
2
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
740
3
|
export function checkOwasp(cwd) {
|
|
741
4
|
const allConfigFiles = collectAgentConfigFiles(cwd);
|
|
@@ -757,7 +20,7 @@ export function checkOwasp(cwd) {
|
|
|
757
20
|
checkASI01(cwd, allConfigFiles),
|
|
758
21
|
checkASI02(cwd, mcpFiles),
|
|
759
22
|
checkASI03(cwd, allConfigFiles),
|
|
760
|
-
checkASI04(cwd, mcpFiles),
|
|
23
|
+
checkASI04(cwd, mcpFiles, allConfigFiles),
|
|
761
24
|
checkASI05(cwd, allConfigFiles),
|
|
762
25
|
checkASI06(cwd),
|
|
763
26
|
checkASI07(cwd, allConfigFiles),
|