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