@safetnsr/vet 1.7.0 → 1.8.2
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/debt.js +17 -4
- package/dist/checks/deps.js +146 -135
- package/dist/checks/diff.js +23 -4
- package/dist/checks/integrity.js +94 -17
- package/dist/checks/memory.js +6 -10
- package/dist/checks/models.js +60 -13
- package/dist/checks/owasp/asi01-goal-hijack.d.ts +5 -0
- package/dist/checks/owasp/asi01-goal-hijack.js +49 -0
- package/dist/checks/owasp/asi02-tool-misuse.d.ts +5 -0
- package/dist/checks/owasp/asi02-tool-misuse.js +98 -0
- package/dist/checks/owasp/asi03-identity-abuse.d.ts +5 -0
- package/dist/checks/owasp/asi03-identity-abuse.js +80 -0
- package/dist/checks/owasp/asi04-supply-chain.d.ts +5 -0
- package/dist/checks/owasp/asi04-supply-chain.js +79 -0
- package/dist/checks/owasp/asi05-code-execution.d.ts +5 -0
- package/dist/checks/owasp/asi05-code-execution.js +67 -0
- package/dist/checks/owasp/asi06-memory-poisoning.d.ts +5 -0
- package/dist/checks/owasp/asi06-memory-poisoning.js +61 -0
- package/dist/checks/owasp/asi07-inter-agent.d.ts +5 -0
- package/dist/checks/owasp/asi07-inter-agent.js +62 -0
- package/dist/checks/owasp/asi08-cascading.d.ts +5 -0
- package/dist/checks/owasp/asi08-cascading.js +36 -0
- package/dist/checks/owasp/asi09-trust-exploitation.d.ts +5 -0
- package/dist/checks/owasp/asi09-trust-exploitation.js +65 -0
- package/dist/checks/owasp/asi10-rogue-agents.d.ts +5 -0
- package/dist/checks/owasp/asi10-rogue-agents.js +31 -0
- package/dist/checks/owasp/index.d.ts +11 -0
- package/dist/checks/owasp/index.js +11 -0
- package/dist/checks/owasp/shared.d.ts +11 -0
- package/dist/checks/owasp/shared.js +61 -0
- package/dist/checks/owasp.js +1 -1
- package/dist/checks/ready.js +42 -7
- package/dist/checks/receipt.js +2 -16
- package/dist/checks/scan.js +54 -6
- package/dist/checks/secrets.js +2 -20
- package/dist/checks/tests.d.ts +3 -0
- package/dist/checks/tests.js +10 -0
- package/dist/checks/verify.js +35 -2
- package/dist/cli.js +111 -69
- package/dist/util.d.ts +0 -1
- package/dist/util.js +1 -1
- package/package.json +1 -1
package/dist/checks/models.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { join, basename } from 'node:path';
|
|
2
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
2
3
|
import { readFile, walkFiles } from '../util.js';
|
|
3
4
|
// ── AI framework detection ───────────────────────────────────────────────────
|
|
4
|
-
const AI_NAME_KEYWORDS = ['ai', 'llm', '
|
|
5
|
+
const AI_NAME_KEYWORDS = ['ai', 'llm', 'openai', 'anthropic', 'langchain', 'provider'];
|
|
5
6
|
const AI_PKG_KEYWORDS = new Set(['ai', 'llm', 'language-model', 'openai', 'anthropic']);
|
|
6
7
|
function isAiFramework(cwd) {
|
|
8
|
+
const aiDeps = ['openai', 'anthropic', 'langchain', 'transformers', 'torch', 'tensorflow', 'llama', 'huggingface'];
|
|
7
9
|
// Check package.json
|
|
8
10
|
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
9
11
|
if (pkgRaw) {
|
|
@@ -17,17 +19,57 @@ function isAiFramework(cwd) {
|
|
|
17
19
|
}
|
|
18
20
|
catch { /* skip */ }
|
|
19
21
|
}
|
|
20
|
-
// Check pyproject.toml / setup.py for
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
// Check pyproject.toml / setup.py in root AND subdirectories (up to 2 levels deep for monorepos)
|
|
23
|
+
const pyprojectPaths = [join(cwd, 'pyproject.toml')];
|
|
24
|
+
try {
|
|
25
|
+
const entries = readdirSync(cwd);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (entry.startsWith('.') || entry === 'node_modules')
|
|
28
|
+
continue;
|
|
29
|
+
const subPath = join(cwd, entry);
|
|
30
|
+
const subPyproject = join(subPath, 'pyproject.toml');
|
|
31
|
+
if (existsSync(subPyproject))
|
|
32
|
+
pyprojectPaths.push(subPyproject);
|
|
33
|
+
// Check 2 levels deep for monorepos (e.g., libs/langchain/pyproject.toml)
|
|
34
|
+
try {
|
|
35
|
+
const subEntries = readdirSync(subPath);
|
|
36
|
+
for (const subEntry of subEntries) {
|
|
37
|
+
if (subEntry.startsWith('.') || subEntry === 'node_modules')
|
|
38
|
+
continue;
|
|
39
|
+
const deepPyproject = join(subPath, subEntry, 'pyproject.toml');
|
|
40
|
+
if (existsSync(deepPyproject))
|
|
41
|
+
pyprojectPaths.push(deepPyproject);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* not a directory or unreadable */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch { /* skip */ }
|
|
48
|
+
for (const pyprojectPath of pyprojectPaths) {
|
|
49
|
+
const pyproject = readFile(pyprojectPath);
|
|
50
|
+
if (pyproject && aiDeps.some(d => pyproject.includes(d)))
|
|
25
51
|
return true;
|
|
26
52
|
}
|
|
27
53
|
const setupPy = readFile(join(cwd, 'setup.py'));
|
|
28
|
-
if (setupPy)
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
if (setupPy && aiDeps.some(d => setupPy.includes(d)))
|
|
55
|
+
return true;
|
|
56
|
+
// Check directory name for AI keywords (use word-boundary-like matching with separators)
|
|
57
|
+
const dirName = basename(cwd).toLowerCase();
|
|
58
|
+
const dirParts = dirName.split(/[-_./\\]/);
|
|
59
|
+
const DIR_AI_KEYWORDS = ['ai', 'llm', 'openai', 'anthropic', 'langchain', 'pydantic-ai', 'autogen', 'crewai'];
|
|
60
|
+
if (DIR_AI_KEYWORDS.some(k => dirParts.includes(k) || dirName.includes(k)))
|
|
61
|
+
return true;
|
|
62
|
+
// Check CLAUDE.md or .claude/settings.json for AI/LLM terms
|
|
63
|
+
const claudeMd = readFile(join(cwd, 'CLAUDE.md'));
|
|
64
|
+
if (claudeMd) {
|
|
65
|
+
const aiTerms = /\b(llm|language model|ai agent|openai|anthropic|embedding|vector|rag|prompt|fine.?tun)/i;
|
|
66
|
+
if (aiTerms.test(claudeMd))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const claudeSettings = readFile(join(cwd, '.claude', 'settings.json'));
|
|
70
|
+
if (claudeSettings) {
|
|
71
|
+
const aiTerms = /\b(llm|language model|ai|openai|anthropic|model)/i;
|
|
72
|
+
if (aiTerms.test(claudeSettings))
|
|
31
73
|
return true;
|
|
32
74
|
}
|
|
33
75
|
return false;
|
|
@@ -200,8 +242,13 @@ function builtinModels(cwd, ignore) {
|
|
|
200
242
|
};
|
|
201
243
|
}
|
|
202
244
|
export async function checkModels(cwd, ignore) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
245
|
+
try {
|
|
246
|
+
const rich = await tryModelGraveyard(cwd);
|
|
247
|
+
if (rich)
|
|
248
|
+
return rich;
|
|
249
|
+
return builtinModels(cwd, ignore);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return { name: 'models', score: 100, maxScore: 100, issues: [], summary: 'models check failed' };
|
|
253
|
+
}
|
|
207
254
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readTextFile } from './shared.js';
|
|
3
|
+
// ── ASI01 — Agent Goal Hijack (prompt injection) ──────────────────────────────
|
|
4
|
+
export function checkASI01(cwd, configFiles) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
if (configFiles.length === 0)
|
|
7
|
+
return { findings, deduction: 0 };
|
|
8
|
+
const injectionKeywords = ['untrusted', 'injection', 'sanitize', 'validate input', 'input boundary', 'prompt injection', 'adversarial'];
|
|
9
|
+
const urlFetchPatterns = /(?:fetch|curl|wget|http\.get|axios\.get|request\.get)\s*\(/i;
|
|
10
|
+
const sanitizationKeywords = ['sanitize', 'escape', 'encode', 'validate', 'strip', 'clean'];
|
|
11
|
+
let injectionAwarenessFound = false;
|
|
12
|
+
for (const filePath of configFiles) {
|
|
13
|
+
const content = readTextFile(filePath);
|
|
14
|
+
if (!content)
|
|
15
|
+
continue;
|
|
16
|
+
const contentLower = content.toLowerCase();
|
|
17
|
+
if (injectionKeywords.some(kw => contentLower.includes(kw))) {
|
|
18
|
+
injectionAwarenessFound = true;
|
|
19
|
+
}
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
if (urlFetchPatterns.test(line)) {
|
|
24
|
+
const context = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 6)).join('\n').toLowerCase();
|
|
25
|
+
const hasSanitization = sanitizationKeywords.some(kw => context.includes(kw));
|
|
26
|
+
if (!hasSanitization) {
|
|
27
|
+
findings.push({
|
|
28
|
+
asiId: 'ASI01',
|
|
29
|
+
severity: 'warning',
|
|
30
|
+
message: 'ASI01: URL fetch instruction without sanitization guidance',
|
|
31
|
+
file: relative(cwd, filePath),
|
|
32
|
+
line: i + 1,
|
|
33
|
+
fixHint: 'add guidance to sanitize/validate content fetched from external URLs',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!injectionAwarenessFound) {
|
|
40
|
+
findings.push({
|
|
41
|
+
asiId: 'ASI01',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
message: 'ASI01: no prompt injection awareness — agent configs do not mention input validation or untrusted content handling',
|
|
44
|
+
fixHint: 'add instructions about handling untrusted input and prompt injection risks to your agent config',
|
|
45
|
+
});
|
|
46
|
+
return { findings, deduction: 15 };
|
|
47
|
+
}
|
|
48
|
+
return { findings, deduction: 0 };
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readTextFile } from './shared.js';
|
|
4
|
+
// ── ASI02 — Tool Misuse and Exploitation ──────────────────────────────────────
|
|
5
|
+
export function checkASI02(cwd, mcpFiles) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
let deduction = 0;
|
|
8
|
+
for (const filePath of mcpFiles) {
|
|
9
|
+
const content = readTextFile(filePath);
|
|
10
|
+
if (!content)
|
|
11
|
+
continue;
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(content);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const relPath = relative(cwd, filePath);
|
|
20
|
+
const mcpConfig = parsed;
|
|
21
|
+
const servers = (mcpConfig.mcpServers ?? mcpConfig.servers ?? {});
|
|
22
|
+
for (const [toolName, toolConfig] of Object.entries(servers)) {
|
|
23
|
+
const tool = toolConfig;
|
|
24
|
+
const hasPermissions = tool.permissions != null || tool.allowedPaths != null || tool.restrictions != null;
|
|
25
|
+
const hasEnv = tool.env != null;
|
|
26
|
+
const args = Array.isArray(tool.args) ? tool.args : [];
|
|
27
|
+
const isShellLike = ['bash', 'sh', 'zsh', 'powershell', 'cmd', 'exec'].some(s => toolName.toLowerCase().includes(s) || args.some((a) => typeof a === 'string' && a.includes(s)));
|
|
28
|
+
if (isShellLike && !hasPermissions) {
|
|
29
|
+
findings.push({
|
|
30
|
+
asiId: 'ASI02',
|
|
31
|
+
severity: 'error',
|
|
32
|
+
message: `ASI02: shell/exec tool "${toolName}" has no permission restrictions`,
|
|
33
|
+
file: relPath,
|
|
34
|
+
fixHint: 'add allowedPaths or restrictions to scope tool access',
|
|
35
|
+
});
|
|
36
|
+
deduction = Math.min(deduction + 10, 30);
|
|
37
|
+
}
|
|
38
|
+
else if (!hasPermissions && !hasEnv) {
|
|
39
|
+
findings.push({
|
|
40
|
+
asiId: 'ASI02',
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
message: `ASI02: MCP tool "${toolName}" has no permission restrictions or path scoping`,
|
|
43
|
+
file: relPath,
|
|
44
|
+
fixHint: 'scope MCP tools with allowedPaths, permissions, or restrictions fields',
|
|
45
|
+
});
|
|
46
|
+
deduction = Math.min(deduction + 10, 30);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof mcpConfig.allowedTools === 'string' && mcpConfig.allowedTools.includes('*')) {
|
|
50
|
+
findings.push({
|
|
51
|
+
asiId: 'ASI02',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
message: 'ASI02: allowedTools contains wildcard — overly broad tool access',
|
|
54
|
+
file: relPath,
|
|
55
|
+
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
56
|
+
});
|
|
57
|
+
deduction = Math.min(deduction + 10, 30);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(mcpConfig.allowedTools)) {
|
|
60
|
+
const hasWildcard = mcpConfig.allowedTools.some(t => t === '*' || t === '**');
|
|
61
|
+
if (hasWildcard) {
|
|
62
|
+
findings.push({
|
|
63
|
+
asiId: 'ASI02',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
message: 'ASI02: allowedTools contains wildcard — overly broad tool access',
|
|
66
|
+
file: relPath,
|
|
67
|
+
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
68
|
+
});
|
|
69
|
+
deduction = Math.min(deduction + 10, 30);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const claudeSettings = join(cwd, '.claude', 'settings.json');
|
|
74
|
+
if (existsSync(claudeSettings)) {
|
|
75
|
+
const content = readTextFile(claudeSettings);
|
|
76
|
+
if (content) {
|
|
77
|
+
try {
|
|
78
|
+
const settings = JSON.parse(content);
|
|
79
|
+
if (Array.isArray(settings.allowedTools)) {
|
|
80
|
+
const tools = settings.allowedTools;
|
|
81
|
+
const hasWildcard = tools.some(t => t === '*' || t === '**' || (typeof t === 'string' && t.endsWith(':*')));
|
|
82
|
+
if (hasWildcard) {
|
|
83
|
+
findings.push({
|
|
84
|
+
asiId: 'ASI02',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
message: 'ASI02: .claude/settings.json allowedTools contains wildcard pattern',
|
|
87
|
+
file: '.claude/settings.json',
|
|
88
|
+
fixHint: 'enumerate specific allowed tools instead of using wildcards',
|
|
89
|
+
});
|
|
90
|
+
deduction = Math.min(deduction + 10, 30);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* intentional: skip unparseable settings */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { findings, deduction };
|
|
98
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readTextFile } from './shared.js';
|
|
3
|
+
// ── ASI03 — Identity and Privilege Abuse ──────────────────────────────────────
|
|
4
|
+
export function checkASI03(cwd, configFiles) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
let deduction = 0;
|
|
7
|
+
const credentialPatterns = [
|
|
8
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*["']?[A-Za-z0-9\-_]{16,}/i, label: 'API key' },
|
|
9
|
+
{ pattern: /(?:secret|token|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9\-_+/]{16,}/i, label: 'secret/token' },
|
|
10
|
+
{ pattern: /ssh-(?:rsa|ed25519|ecdsa)\s+[A-Za-z0-9+/]{20,}/i, label: 'SSH public key' },
|
|
11
|
+
{ pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i, label: 'private key' },
|
|
12
|
+
{ pattern: /(?:AWS|GOOGLE|AZURE)_[A-Z_]+\s*[=:]\s*["']?[A-Za-z0-9/+]{16,}/i, label: 'cloud credential' },
|
|
13
|
+
];
|
|
14
|
+
const sudoPattern = /\bsudo\b|\bsu\s+-\b|run as root|elevated privileges|administrator rights/i;
|
|
15
|
+
const leastPrivKeywords = ['least.?privilege', 'minimum.?permission', 'scoped credentials', 'credential scop', 'no credentials', 'avoid.*key', 'don.*t.*store.*key'];
|
|
16
|
+
let hasLeastPrivMention = false;
|
|
17
|
+
let hasEnvFileRef = false;
|
|
18
|
+
for (const filePath of configFiles) {
|
|
19
|
+
const content = readTextFile(filePath);
|
|
20
|
+
if (!content)
|
|
21
|
+
continue;
|
|
22
|
+
const contentLower = content.toLowerCase();
|
|
23
|
+
const relPath = relative(cwd, filePath);
|
|
24
|
+
if (leastPrivKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
25
|
+
hasLeastPrivMention = true;
|
|
26
|
+
}
|
|
27
|
+
if (!filePath.endsWith('.env') && !filePath.includes('.env.')) {
|
|
28
|
+
if (contentLower.includes('.env') && /load|source|read|require|import/i.test(content)) {
|
|
29
|
+
if (!hasEnvFileRef) {
|
|
30
|
+
findings.push({
|
|
31
|
+
asiId: 'ASI03',
|
|
32
|
+
severity: 'warning',
|
|
33
|
+
message: 'ASI03: agent config references .env file — credentials may be accessible to agent',
|
|
34
|
+
file: relPath,
|
|
35
|
+
fixHint: 'ensure .env is not exposed to agent scope; use secret managers or scoped env vars',
|
|
36
|
+
});
|
|
37
|
+
deduction = Math.min(deduction + 15, 30);
|
|
38
|
+
hasEnvFileRef = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i];
|
|
45
|
+
for (const { pattern, label } of credentialPatterns) {
|
|
46
|
+
if (pattern.test(line)) {
|
|
47
|
+
findings.push({
|
|
48
|
+
asiId: 'ASI03',
|
|
49
|
+
severity: 'error',
|
|
50
|
+
message: `ASI03: possible ${label} in agent config`,
|
|
51
|
+
file: relPath,
|
|
52
|
+
line: i + 1,
|
|
53
|
+
fixHint: 'remove credentials from agent configs; use environment variables or secret managers',
|
|
54
|
+
});
|
|
55
|
+
deduction = Math.min(deduction + 15, 30);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (sudoPattern.test(line)) {
|
|
59
|
+
findings.push({
|
|
60
|
+
asiId: 'ASI03',
|
|
61
|
+
severity: 'warning',
|
|
62
|
+
message: 'ASI03: agent config grants sudo/root access — privilege escalation risk',
|
|
63
|
+
file: relPath,
|
|
64
|
+
line: i + 1,
|
|
65
|
+
fixHint: 'restrict agent to least-privilege — avoid sudo and root access in agent instructions',
|
|
66
|
+
});
|
|
67
|
+
deduction = Math.min(deduction + 15, 30);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!hasLeastPrivMention && configFiles.length > 0) {
|
|
72
|
+
findings.push({
|
|
73
|
+
asiId: 'ASI03',
|
|
74
|
+
severity: 'info',
|
|
75
|
+
message: 'ASI03: no mention of least-privilege or credential scoping in agent configs',
|
|
76
|
+
fixHint: 'document credential access policies and least-privilege principles in agent config',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { findings, deduction };
|
|
80
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readTextFile } from './shared.js';
|
|
3
|
+
// ── ASI04 — Agentic Supply Chain Vulnerabilities ──────────────────────────────
|
|
4
|
+
export function checkASI04(cwd, mcpFiles, agentConfigFiles) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
let deduction = 0;
|
|
7
|
+
const localhostPattern = /^(https?:\/\/)?localhost|^(https?:\/\/)?127\.|^(https?:\/\/)?0\.0\.0\.0/i;
|
|
8
|
+
for (const filePath of mcpFiles) {
|
|
9
|
+
const content = readTextFile(filePath);
|
|
10
|
+
if (!content)
|
|
11
|
+
continue;
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(content);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const relPath = relative(cwd, filePath);
|
|
20
|
+
const mcpConfig = parsed;
|
|
21
|
+
const servers = (mcpConfig.mcpServers ?? mcpConfig.servers ?? {});
|
|
22
|
+
for (const [toolName, toolConfig] of Object.entries(servers)) {
|
|
23
|
+
const tool = toolConfig;
|
|
24
|
+
const url = typeof tool.url === 'string' ? tool.url : null;
|
|
25
|
+
const command = typeof tool.command === 'string' ? tool.command : null;
|
|
26
|
+
const version = typeof tool.version === 'string' ? tool.version : null;
|
|
27
|
+
if (url && !localhostPattern.test(url)) {
|
|
28
|
+
findings.push({
|
|
29
|
+
asiId: 'ASI04',
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
message: `ASI04: MCP server "${toolName}" points to external URL: ${url}`,
|
|
32
|
+
file: relPath,
|
|
33
|
+
fixHint: 'verify external MCP servers; prefer localhost/self-hosted; pin versions',
|
|
34
|
+
});
|
|
35
|
+
deduction += 10;
|
|
36
|
+
}
|
|
37
|
+
if (command && !version) {
|
|
38
|
+
if (/npx\s+[^@\s]+(?!\s*@)/.test(command) || (command === 'npx' && !version)) {
|
|
39
|
+
const args = Array.isArray(tool.args) ? tool.args : [];
|
|
40
|
+
const firstArg = args[0];
|
|
41
|
+
if (typeof firstArg === 'string' && !firstArg.includes('@') && !firstArg.startsWith('-')) {
|
|
42
|
+
findings.push({
|
|
43
|
+
asiId: 'ASI04',
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
message: `ASI04: MCP tool "${toolName}" uses unpinned npx package "${firstArg}"`,
|
|
46
|
+
file: relPath,
|
|
47
|
+
fixHint: 'pin package versions (e.g. @scope/package@1.2.3) to prevent supply chain attacks',
|
|
48
|
+
});
|
|
49
|
+
deduction += 10;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const unpinnedNpmPattern = /npm\s+i(?:nstall)?\s+(?:@[^/]+\/)?[a-z][a-z0-9\-]*(?!\s*@[0-9])/i;
|
|
56
|
+
for (const filePath of agentConfigFiles) {
|
|
57
|
+
const content = readTextFile(filePath);
|
|
58
|
+
if (!content)
|
|
59
|
+
continue;
|
|
60
|
+
const relPath = relative(cwd, filePath);
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
if (unpinnedNpmPattern.test(line) && !/pinned|verified|trusted/i.test(line)) {
|
|
65
|
+
findings.push({
|
|
66
|
+
asiId: 'ASI04',
|
|
67
|
+
severity: 'info',
|
|
68
|
+
message: 'ASI04: agent config references npm package without pinned version',
|
|
69
|
+
file: relPath,
|
|
70
|
+
line: i + 1,
|
|
71
|
+
fixHint: 'pin npm package versions in agent instructions to prevent supply chain attacks',
|
|
72
|
+
});
|
|
73
|
+
deduction += 10;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { findings, deduction };
|
|
79
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readTextFile } from './shared.js';
|
|
3
|
+
// ── ASI05 — Unexpected Code Execution ────────────────────────────────────────
|
|
4
|
+
export function checkASI05(cwd, configFiles) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
if (configFiles.length === 0)
|
|
7
|
+
return { findings, deduction: 0 };
|
|
8
|
+
const unrestrictedExecPattern = /(?:allow|can|may|enabled?)\s+(?:to\s+)?(?:run|execute|exec|eval|shell)/i;
|
|
9
|
+
const sandboxKeywords = ['sandbox', 'container', 'docker', 'isolated', 'restricted', 'approval', 'review before', 'confirm before'];
|
|
10
|
+
const codeApprovalKeywords = ['review', 'approve', 'confirm', 'gate', 'human.?in.?the.?loop', 'ask before'];
|
|
11
|
+
let hasSandboxMention = false;
|
|
12
|
+
let hasCodeApproval = false;
|
|
13
|
+
let hasUnrestrictedExec = false;
|
|
14
|
+
for (const filePath of configFiles) {
|
|
15
|
+
const content = readTextFile(filePath);
|
|
16
|
+
if (!content)
|
|
17
|
+
continue;
|
|
18
|
+
if (sandboxKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
19
|
+
hasSandboxMention = true;
|
|
20
|
+
}
|
|
21
|
+
if (codeApprovalKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
22
|
+
hasCodeApproval = true;
|
|
23
|
+
}
|
|
24
|
+
const lines = content.split('\n');
|
|
25
|
+
const relPath = relative(cwd, filePath);
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
if (/autoApprove|auto.?approve/i.test(line) && !/false|disabled?|no\s+auto/i.test(line)) {
|
|
29
|
+
findings.push({
|
|
30
|
+
asiId: 'ASI05',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: 'ASI05: autoApprove pattern detected — code execution may proceed without human review',
|
|
33
|
+
file: relPath,
|
|
34
|
+
line: i + 1,
|
|
35
|
+
fixHint: 'require human approval gates before executing generated code',
|
|
36
|
+
});
|
|
37
|
+
hasUnrestrictedExec = true;
|
|
38
|
+
}
|
|
39
|
+
if (unrestrictedExecPattern.test(line) && !sandboxKeywords.some(kw => new RegExp(kw, 'i').test(line))) {
|
|
40
|
+
findings.push({
|
|
41
|
+
asiId: 'ASI05',
|
|
42
|
+
severity: 'info',
|
|
43
|
+
message: 'ASI05: agent config allows code execution — ensure sandbox/approval controls are in place',
|
|
44
|
+
file: relPath,
|
|
45
|
+
line: i + 1,
|
|
46
|
+
fixHint: 'add sandbox restrictions or approval gates for code execution',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (hasUnrestrictedExec || (!hasSandboxMention && !hasCodeApproval)) {
|
|
52
|
+
const hasExecContent = configFiles.some(f => {
|
|
53
|
+
const c = readTextFile(f);
|
|
54
|
+
return c && /exec|shell|run|execute|eval|bash|python|node/i.test(c);
|
|
55
|
+
});
|
|
56
|
+
if (hasExecContent && !hasSandboxMention && !hasCodeApproval) {
|
|
57
|
+
findings.push({
|
|
58
|
+
asiId: 'ASI05',
|
|
59
|
+
severity: 'warning',
|
|
60
|
+
message: 'ASI05: agent config references code execution without sandbox or approval gates',
|
|
61
|
+
fixHint: 'document sandbox restrictions and require approval for code execution in agent configs',
|
|
62
|
+
});
|
|
63
|
+
return { findings, deduction: 10 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { findings, deduction: hasUnrestrictedExec ? 10 : 0 };
|
|
67
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { readTextFile } from './shared.js';
|
|
4
|
+
// ── ASI06 — Memory and Context Poisoning ─────────────────────────────────────
|
|
5
|
+
export function checkASI06(cwd) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
let deduction = 0;
|
|
8
|
+
const memoryPaths = [
|
|
9
|
+
'.claude/memory',
|
|
10
|
+
'.cursor/memory',
|
|
11
|
+
'memory',
|
|
12
|
+
'.aider.chat.history.md',
|
|
13
|
+
'.continue/memory',
|
|
14
|
+
'agent-memory',
|
|
15
|
+
'context-store',
|
|
16
|
+
];
|
|
17
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
18
|
+
let gitignoreContent = '';
|
|
19
|
+
try {
|
|
20
|
+
gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch { /* intentional: .gitignore may not exist */ }
|
|
23
|
+
for (const memPath of memoryPaths) {
|
|
24
|
+
const full = join(cwd, memPath);
|
|
25
|
+
if (!existsSync(full))
|
|
26
|
+
continue;
|
|
27
|
+
const isIgnored = gitignoreContent.includes(memPath) || gitignoreContent.includes(memPath.split('/')[0]);
|
|
28
|
+
if (!isIgnored) {
|
|
29
|
+
findings.push({
|
|
30
|
+
asiId: 'ASI06',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `ASI06: agent memory path "${memPath}" is not in .gitignore — could be poisoned via PR`,
|
|
33
|
+
file: memPath,
|
|
34
|
+
fixHint: 'add agent memory directories to .gitignore to prevent context poisoning via PRs',
|
|
35
|
+
});
|
|
36
|
+
deduction += 8;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const ragPatterns = ['.continue/config.json', '.cursor/settings.json'];
|
|
40
|
+
for (const ragPath of ragPatterns) {
|
|
41
|
+
const full = join(cwd, ragPath);
|
|
42
|
+
if (!existsSync(full))
|
|
43
|
+
continue;
|
|
44
|
+
const content = readTextFile(full);
|
|
45
|
+
if (!content)
|
|
46
|
+
continue;
|
|
47
|
+
const hasRag = /embed|rag|retrieval|vector|index/i.test(content);
|
|
48
|
+
const hasFiltering = /filter|sanitize|validate|allowlist|blocklist/i.test(content);
|
|
49
|
+
if (hasRag && !hasFiltering) {
|
|
50
|
+
findings.push({
|
|
51
|
+
asiId: 'ASI06',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
message: `ASI06: RAG/embedding config "${ragPath}" has no input filtering`,
|
|
54
|
+
file: ragPath,
|
|
55
|
+
fixHint: 'add input filtering and validation for RAG/embedding sources to prevent context poisoning',
|
|
56
|
+
});
|
|
57
|
+
deduction += 8;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { findings, deduction };
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { readTextFile } from './shared.js';
|
|
3
|
+
// ── ASI07 — Insecure Inter-Agent Communication ───────────────────────────────
|
|
4
|
+
export function checkASI07(cwd, configFiles) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
let deduction = 0;
|
|
7
|
+
const multiAgentKeywords = [
|
|
8
|
+
'a2a', 'agent-to-agent', 'multi.?agent', 'subagent', 'sub-agent',
|
|
9
|
+
'spawn agent', 'delegate', 'orchestrat', 'swarm', 'crew', 'autogen',
|
|
10
|
+
];
|
|
11
|
+
const authKeywords = ['auth', 'token', 'signed', 'hmac', 'jwt', 'api.?key', 'verify', 'authenticated'];
|
|
12
|
+
const encryptionKeywords = ['encrypt', 'tls', 'ssl', 'https', 'secure channel'];
|
|
13
|
+
let isMultiAgentSetup = false;
|
|
14
|
+
let hasAuthMention = false;
|
|
15
|
+
for (const filePath of configFiles) {
|
|
16
|
+
const content = readTextFile(filePath);
|
|
17
|
+
if (!content)
|
|
18
|
+
continue;
|
|
19
|
+
const hasMultiAgent = multiAgentKeywords.some(kw => new RegExp(kw, 'i').test(content));
|
|
20
|
+
if (hasMultiAgent) {
|
|
21
|
+
isMultiAgentSetup = true;
|
|
22
|
+
const relPath = relative(cwd, filePath);
|
|
23
|
+
if (authKeywords.some(kw => new RegExp(kw, 'i').test(content))) {
|
|
24
|
+
hasAuthMention = true;
|
|
25
|
+
}
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
const lineHasMultiAgent = multiAgentKeywords.some(kw => new RegExp(kw, 'i').test(line));
|
|
30
|
+
if (lineHasMultiAgent) {
|
|
31
|
+
const context = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join('\n');
|
|
32
|
+
const hasLocalAuth = authKeywords.some(kw => new RegExp(kw, 'i').test(context));
|
|
33
|
+
const hasLocalEncrypt = encryptionKeywords.some(kw => new RegExp(kw, 'i').test(context));
|
|
34
|
+
if (!hasLocalAuth && !hasLocalEncrypt) {
|
|
35
|
+
findings.push({
|
|
36
|
+
asiId: 'ASI07',
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
message: 'ASI07: inter-agent communication pattern without authentication or encryption context',
|
|
39
|
+
file: relPath,
|
|
40
|
+
line: i + 1,
|
|
41
|
+
fixHint: 'ensure agent-to-agent channels use authentication tokens and encrypted transport',
|
|
42
|
+
});
|
|
43
|
+
deduction += 8;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (isMultiAgentSetup && !hasAuthMention) {
|
|
51
|
+
if (!findings.some(f => f.asiId === 'ASI07')) {
|
|
52
|
+
findings.push({
|
|
53
|
+
asiId: 'ASI07',
|
|
54
|
+
severity: 'warning',
|
|
55
|
+
message: 'ASI07: multi-agent setup detected but no authentication mentioned for inter-agent communication',
|
|
56
|
+
fixHint: 'add authentication and authorization requirements for agent-to-agent communication',
|
|
57
|
+
});
|
|
58
|
+
deduction = Math.min(deduction + 8, 24);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { findings, deduction };
|
|
62
|
+
}
|