@nerviq/cli 1.29.0 → 1.29.1
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/CHANGELOG.md +1527 -1493
- package/README.md +550 -538
- package/SECURITY.md +82 -82
- package/bin/cli.js +2562 -2558
- package/docs/api-reference.md +356 -356
- package/docs/audit-fix.md +109 -0
- package/docs/autofix.md +3 -62
- package/docs/getting-started.md +1 -1
- package/docs/index.html +592 -592
- package/docs/integration-contracts.md +287 -287
- package/docs/maintenance.md +128 -128
- package/docs/new-platform-guide.md +202 -202
- package/docs/release-process.md +63 -0
- package/docs/shallow-risk.md +244 -244
- package/docs/why-nerviq.md +82 -82
- package/package.json +67 -67
- package/src/aider/activity.js +226 -226
- package/src/aider/context.js +162 -162
- package/src/aider/freshness.js +123 -123
- package/src/aider/techniques.js +3465 -3465
- package/src/audit/layers.js +180 -180
- package/src/audit.js +1032 -1032
- package/src/benchmark.js +299 -299
- package/src/codex/activity.js +324 -324
- package/src/codex/freshness.js +142 -142
- package/src/codex/techniques.js +4895 -4895
- package/src/context.js +326 -326
- package/src/continuous-ops.js +11 -1
- package/src/convert.js +340 -340
- package/src/copilot/config-parser.js +280 -280
- package/src/copilot/context.js +218 -218
- package/src/copilot/freshness.js +177 -177
- package/src/copilot/patch.js +238 -238
- package/src/copilot/techniques.js +3578 -3578
- package/src/cursor/freshness.js +194 -194
- package/src/cursor/patch.js +243 -243
- package/src/cursor/techniques.js +3735 -3735
- package/src/doctor.js +201 -201
- package/src/fix-engine.js +511 -8
- package/src/formatters/csv.js +86 -86
- package/src/formatters/junit.js +123 -123
- package/src/formatters/markdown.js +164 -164
- package/src/formatters/otel.js +151 -151
- package/src/freshness.js +156 -156
- package/src/gemini/activity.js +402 -402
- package/src/gemini/context.js +290 -290
- package/src/gemini/freshness.js +183 -183
- package/src/gemini/patch.js +229 -229
- package/src/gemini/techniques.js +3811 -3811
- package/src/governance.js +533 -533
- package/src/harmony/audit.js +306 -306
- package/src/i18n.js +63 -63
- package/src/insights.js +119 -119
- package/src/integrations.js +134 -134
- package/src/locales/en.json +33 -33
- package/src/locales/es.json +33 -33
- package/src/migrate.js +354 -354
- package/src/opencode/activity.js +286 -286
- package/src/opencode/freshness.js +137 -137
- package/src/opencode/techniques.js +3450 -3450
- package/src/setup/analysis.js +12 -12
- package/src/setup.js +7 -6
- package/src/shallow-risk/index.js +56 -56
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
- package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
- package/src/shallow-risk/shared.js +648 -648
- package/src/source-urls.js +295 -295
- package/src/state-paths.js +85 -85
- package/src/supplemental-checks.js +805 -805
- package/src/telemetry.js +160 -160
- package/src/windsurf/context.js +359 -359
- package/src/windsurf/freshness.js +194 -194
- package/src/windsurf/patch.js +231 -231
- package/src/windsurf/techniques.js +3779 -3779
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { SHALLOW_RISK_DOC_URL, getAgentConfigEntries } = require('../shared');
|
|
4
|
-
|
|
5
|
-
const SECRET_PATTERNS = [
|
|
6
|
-
{ label: 'AWS access key', pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
7
|
-
{ label: 'Stripe live key', pattern: /\bsk_live_[A-Za-z0-9]{24,}\b/g },
|
|
8
|
-
{ label: 'GitHub personal access token', pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
9
|
-
{ label: 'SSH private key header', pattern: /-----BEGIN (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----/g },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
function looksLikePlaceholder(line, matchText) {
|
|
13
|
-
return /\b(example|sample|placeholder|replace[-_ ]?me|your[_-]?key|fake|dummy)\b/i.test(line) ||
|
|
14
|
-
/\bEXAMPLE\b/.test(matchText);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
module.exports = {
|
|
18
|
-
key: 'agent-config-secret-literal',
|
|
19
|
-
name: 'Agent config contains secret literal',
|
|
20
|
-
severity: 'critical',
|
|
21
|
-
layer: 'shallow-risk',
|
|
22
|
-
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
23
|
-
run(ctx) {
|
|
24
|
-
const findings = [];
|
|
25
|
-
|
|
26
|
-
for (const entry of getAgentConfigEntries(ctx)) {
|
|
27
|
-
const lines = entry.content.split(/\r?\n/);
|
|
28
|
-
for (let index = 0; index < lines.length; index++) {
|
|
29
|
-
const line = lines[index];
|
|
30
|
-
for (const secret of SECRET_PATTERNS) {
|
|
31
|
-
secret.pattern.lastIndex = 0;
|
|
32
|
-
let match = secret.pattern.exec(line);
|
|
33
|
-
while (match) {
|
|
34
|
-
if (!looksLikePlaceholder(line, match[0])) {
|
|
35
|
-
findings.push({
|
|
36
|
-
file: entry.path,
|
|
37
|
-
line: index + 1,
|
|
38
|
-
fix: `${entry.path} contains a ${secret.label} shape. Rotate the secret, remove it from the agent config, and scrub it from git history if it was real.`,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
match = secret.pattern.exec(line);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return findings;
|
|
48
|
-
},
|
|
49
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SHALLOW_RISK_DOC_URL, getAgentConfigEntries } = require('../shared');
|
|
4
|
+
|
|
5
|
+
const SECRET_PATTERNS = [
|
|
6
|
+
{ label: 'AWS access key', pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
7
|
+
{ label: 'Stripe live key', pattern: /\bsk_live_[A-Za-z0-9]{24,}\b/g },
|
|
8
|
+
{ label: 'GitHub personal access token', pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
9
|
+
{ label: 'SSH private key header', pattern: /-----BEGIN (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----/g },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function looksLikePlaceholder(line, matchText) {
|
|
13
|
+
return /\b(example|sample|placeholder|replace[-_ ]?me|your[_-]?key|fake|dummy)\b/i.test(line) ||
|
|
14
|
+
/\bEXAMPLE\b/.test(matchText);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
key: 'agent-config-secret-literal',
|
|
19
|
+
name: 'Agent config contains secret literal',
|
|
20
|
+
severity: 'critical',
|
|
21
|
+
layer: 'shallow-risk',
|
|
22
|
+
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
23
|
+
run(ctx) {
|
|
24
|
+
const findings = [];
|
|
25
|
+
|
|
26
|
+
for (const entry of getAgentConfigEntries(ctx)) {
|
|
27
|
+
const lines = entry.content.split(/\r?\n/);
|
|
28
|
+
for (let index = 0; index < lines.length; index++) {
|
|
29
|
+
const line = lines[index];
|
|
30
|
+
for (const secret of SECRET_PATTERNS) {
|
|
31
|
+
secret.pattern.lastIndex = 0;
|
|
32
|
+
let match = secret.pattern.exec(line);
|
|
33
|
+
while (match) {
|
|
34
|
+
if (!looksLikePlaceholder(line, match[0])) {
|
|
35
|
+
findings.push({
|
|
36
|
+
file: entry.path,
|
|
37
|
+
line: index + 1,
|
|
38
|
+
fix: `${entry.path} contains a ${secret.label} shape. Rotate the secret, remove it from the agent config, and scrub it from git history if it was real.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
match = secret.pattern.exec(line);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return findings;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
SHALLOW_RISK_DOC_URL,
|
|
5
|
-
collectStackClaims,
|
|
6
|
-
findFirstStackEvidence,
|
|
7
|
-
getDetectedStackEvidence,
|
|
8
|
-
} = require('../shared');
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
key: 'agent-config-stack-contradiction',
|
|
12
|
-
name: 'Agent config contradicts actual stack',
|
|
13
|
-
severity: 'high',
|
|
14
|
-
layer: 'shallow-risk',
|
|
15
|
-
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
16
|
-
run(ctx) {
|
|
17
|
-
const claims = collectStackClaims(ctx);
|
|
18
|
-
const distinctClaims = [...new Set(claims.map((claim) => claim.key))];
|
|
19
|
-
if (distinctClaims.length !== 1) return [];
|
|
20
|
-
|
|
21
|
-
const declared = claims[0];
|
|
22
|
-
const hasDeclaredEvidence = declared.stackKeys.some((stackKey) => Boolean(findFirstStackEvidence(ctx, stackKey)));
|
|
23
|
-
if (hasDeclaredEvidence) return [];
|
|
24
|
-
|
|
25
|
-
const actual = getDetectedStackEvidence(ctx).find((item) => !declared.stackKeys.includes(item.key));
|
|
26
|
-
if (!actual) return [];
|
|
27
|
-
|
|
28
|
-
return [{
|
|
29
|
-
file: declared.file,
|
|
30
|
-
line: declared.line,
|
|
31
|
-
fix: `${declared.file} declares the primary stack as "${declared.label}", but the repo shows ${actual.label} signals (${actual.file}) and no ${declared.label} evidence. Align the agent guidance with the actual stack or document a real migration plan.`,
|
|
32
|
-
}];
|
|
33
|
-
},
|
|
34
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
SHALLOW_RISK_DOC_URL,
|
|
5
|
+
collectStackClaims,
|
|
6
|
+
findFirstStackEvidence,
|
|
7
|
+
getDetectedStackEvidence,
|
|
8
|
+
} = require('../shared');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
key: 'agent-config-stack-contradiction',
|
|
12
|
+
name: 'Agent config contradicts actual stack',
|
|
13
|
+
severity: 'high',
|
|
14
|
+
layer: 'shallow-risk',
|
|
15
|
+
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
16
|
+
run(ctx) {
|
|
17
|
+
const claims = collectStackClaims(ctx);
|
|
18
|
+
const distinctClaims = [...new Set(claims.map((claim) => claim.key))];
|
|
19
|
+
if (distinctClaims.length !== 1) return [];
|
|
20
|
+
|
|
21
|
+
const declared = claims[0];
|
|
22
|
+
const hasDeclaredEvidence = declared.stackKeys.some((stackKey) => Boolean(findFirstStackEvidence(ctx, stackKey)));
|
|
23
|
+
if (hasDeclaredEvidence) return [];
|
|
24
|
+
|
|
25
|
+
const actual = getDetectedStackEvidence(ctx).find((item) => !declared.stackKeys.includes(item.key));
|
|
26
|
+
if (!actual) return [];
|
|
27
|
+
|
|
28
|
+
return [{
|
|
29
|
+
file: declared.file,
|
|
30
|
+
line: declared.line,
|
|
31
|
+
fix: `${declared.file} declares the primary stack as "${declared.label}", but the repo shows ${actual.label} signals (${actual.file}) and no ${declared.label} evidence. Align the agent guidance with the actual stack or document a real migration plan.`,
|
|
32
|
+
}];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
SHALLOW_RISK_DOC_URL,
|
|
5
|
-
escapeRegExp,
|
|
6
|
-
getHookCommandPath,
|
|
7
|
-
resolveRepoPath,
|
|
8
|
-
} = require('../shared');
|
|
9
|
-
|
|
10
|
-
const HOOK_EVENTS = new Set([
|
|
11
|
-
'PreToolUse',
|
|
12
|
-
'PostToolUse',
|
|
13
|
-
'Stop',
|
|
14
|
-
'UserPromptSubmit',
|
|
15
|
-
'SessionStart',
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
function collectHookCommands(node, output = []) {
|
|
19
|
-
if (Array.isArray(node)) {
|
|
20
|
-
for (const item of node) collectHookCommands(item, output);
|
|
21
|
-
return output;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!node || typeof node !== 'object') {
|
|
25
|
-
return output;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (node.type === 'command' && typeof node.command === 'string') {
|
|
29
|
-
output.push(node.command);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const value of Object.values(node)) {
|
|
33
|
-
collectHookCommands(value, output);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return output;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = {
|
|
40
|
-
key: 'hook-script-missing',
|
|
41
|
-
name: 'Configured hook script is missing',
|
|
42
|
-
severity: 'high',
|
|
43
|
-
layer: 'shallow-risk',
|
|
44
|
-
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
45
|
-
run(ctx) {
|
|
46
|
-
const file = '.claude/settings.json';
|
|
47
|
-
const config = ctx.jsonFile(file);
|
|
48
|
-
if (!config || !config.hooks || typeof config.hooks !== 'object') {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const findings = [];
|
|
53
|
-
for (const [eventName, entries] of Object.entries(config.hooks)) {
|
|
54
|
-
if (!HOOK_EVENTS.has(eventName)) continue;
|
|
55
|
-
for (const command of collectHookCommands(entries)) {
|
|
56
|
-
const scriptPath = getHookCommandPath(command);
|
|
57
|
-
if (!scriptPath) continue;
|
|
58
|
-
const resolvedPath = resolveRepoPath(ctx, file, scriptPath, 'repo-root');
|
|
59
|
-
if (!resolvedPath || ctx.fileContent(resolvedPath) !== null) continue;
|
|
60
|
-
findings.push({
|
|
61
|
-
file,
|
|
62
|
-
line: ctx.lineNumber(file, new RegExp(escapeRegExp(command))) || ctx.lineNumber(file, new RegExp(`"${escapeRegExp(eventName)}"`)) || 1,
|
|
63
|
-
fix: `${file} declares a ${eventName} hook at \`${resolvedPath}\`, but the script is missing. Create the hook file or remove the dead hook reference.`,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return findings;
|
|
69
|
-
},
|
|
70
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
SHALLOW_RISK_DOC_URL,
|
|
5
|
+
escapeRegExp,
|
|
6
|
+
getHookCommandPath,
|
|
7
|
+
resolveRepoPath,
|
|
8
|
+
} = require('../shared');
|
|
9
|
+
|
|
10
|
+
const HOOK_EVENTS = new Set([
|
|
11
|
+
'PreToolUse',
|
|
12
|
+
'PostToolUse',
|
|
13
|
+
'Stop',
|
|
14
|
+
'UserPromptSubmit',
|
|
15
|
+
'SessionStart',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function collectHookCommands(node, output = []) {
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
for (const item of node) collectHookCommands(item, output);
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!node || typeof node !== 'object') {
|
|
25
|
+
return output;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (node.type === 'command' && typeof node.command === 'string') {
|
|
29
|
+
output.push(node.command);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const value of Object.values(node)) {
|
|
33
|
+
collectHookCommands(value, output);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
key: 'hook-script-missing',
|
|
41
|
+
name: 'Configured hook script is missing',
|
|
42
|
+
severity: 'high',
|
|
43
|
+
layer: 'shallow-risk',
|
|
44
|
+
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
45
|
+
run(ctx) {
|
|
46
|
+
const file = '.claude/settings.json';
|
|
47
|
+
const config = ctx.jsonFile(file);
|
|
48
|
+
if (!config || !config.hooks || typeof config.hooks !== 'object') {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const findings = [];
|
|
53
|
+
for (const [eventName, entries] of Object.entries(config.hooks)) {
|
|
54
|
+
if (!HOOK_EVENTS.has(eventName)) continue;
|
|
55
|
+
for (const command of collectHookCommands(entries)) {
|
|
56
|
+
const scriptPath = getHookCommandPath(command);
|
|
57
|
+
if (!scriptPath) continue;
|
|
58
|
+
const resolvedPath = resolveRepoPath(ctx, file, scriptPath, 'repo-root');
|
|
59
|
+
if (!resolvedPath || ctx.fileContent(resolvedPath) !== null) continue;
|
|
60
|
+
findings.push({
|
|
61
|
+
file,
|
|
62
|
+
line: ctx.lineNumber(file, new RegExp(escapeRegExp(command))) || ctx.lineNumber(file, new RegExp(`"${escapeRegExp(eventName)}"`)) || 1,
|
|
63
|
+
fix: `${file} declares a ${eventName} hook at \`${resolvedPath}\`, but the script is missing. Create the hook file or remove the dead hook reference.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
SHALLOW_RISK_DOC_URL,
|
|
5
|
-
escapeRegExp,
|
|
6
|
-
isClearlyLocalMcpBinary,
|
|
7
|
-
} = require('../shared');
|
|
8
|
-
|
|
9
|
-
function hasBroadOpenPermissions(server) {
|
|
10
|
-
if (!server || typeof server !== 'object') return false;
|
|
11
|
-
if (Array.isArray(server.permissions) && server.permissions.length === 0) return true;
|
|
12
|
-
if (server.allow === '*') return true;
|
|
13
|
-
if (Array.isArray(server.allow) && server.allow.includes('*')) return true;
|
|
14
|
-
if (server.permissions && typeof server.permissions === 'object') {
|
|
15
|
-
if (server.permissions.allow === '*') return true;
|
|
16
|
-
if (Array.isArray(server.permissions.allow) && server.permissions.allow.includes('*')) return true;
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
module.exports = {
|
|
22
|
-
key: 'mcp-server-no-allowlist',
|
|
23
|
-
name: 'MCP server has no allowlist',
|
|
24
|
-
severity: 'critical',
|
|
25
|
-
layer: 'shallow-risk',
|
|
26
|
-
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
27
|
-
run(ctx) {
|
|
28
|
-
const findings = [];
|
|
29
|
-
const candidates = ['.claude/settings.json', '.mcp.json'];
|
|
30
|
-
|
|
31
|
-
for (const file of candidates) {
|
|
32
|
-
const config = ctx.jsonFile(file);
|
|
33
|
-
const servers = config && config.mcpServers && typeof config.mcpServers === 'object'
|
|
34
|
-
? config.mcpServers
|
|
35
|
-
: null;
|
|
36
|
-
if (!servers) continue;
|
|
37
|
-
|
|
38
|
-
for (const [serverName, server] of Object.entries(servers)) {
|
|
39
|
-
if (!hasBroadOpenPermissions(server)) continue;
|
|
40
|
-
const command = typeof server.command === 'string' ? server.command : '';
|
|
41
|
-
findings.push({
|
|
42
|
-
severity: isClearlyLocalMcpBinary(command) ? 'high' : 'critical',
|
|
43
|
-
file,
|
|
44
|
-
line: ctx.lineNumber(file, new RegExp(`"${escapeRegExp(serverName)}"`)) || 1,
|
|
45
|
-
fix: `MCP server "${serverName}" in ${file} has broad access without an allowlist. Add a narrow allow/permissions list before relying on it in CI or production repos.`,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return findings;
|
|
51
|
-
},
|
|
52
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
SHALLOW_RISK_DOC_URL,
|
|
5
|
+
escapeRegExp,
|
|
6
|
+
isClearlyLocalMcpBinary,
|
|
7
|
+
} = require('../shared');
|
|
8
|
+
|
|
9
|
+
function hasBroadOpenPermissions(server) {
|
|
10
|
+
if (!server || typeof server !== 'object') return false;
|
|
11
|
+
if (Array.isArray(server.permissions) && server.permissions.length === 0) return true;
|
|
12
|
+
if (server.allow === '*') return true;
|
|
13
|
+
if (Array.isArray(server.allow) && server.allow.includes('*')) return true;
|
|
14
|
+
if (server.permissions && typeof server.permissions === 'object') {
|
|
15
|
+
if (server.permissions.allow === '*') return true;
|
|
16
|
+
if (Array.isArray(server.permissions.allow) && server.permissions.allow.includes('*')) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
key: 'mcp-server-no-allowlist',
|
|
23
|
+
name: 'MCP server has no allowlist',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
layer: 'shallow-risk',
|
|
26
|
+
sourceUrl: SHALLOW_RISK_DOC_URL,
|
|
27
|
+
run(ctx) {
|
|
28
|
+
const findings = [];
|
|
29
|
+
const candidates = ['.claude/settings.json', '.mcp.json'];
|
|
30
|
+
|
|
31
|
+
for (const file of candidates) {
|
|
32
|
+
const config = ctx.jsonFile(file);
|
|
33
|
+
const servers = config && config.mcpServers && typeof config.mcpServers === 'object'
|
|
34
|
+
? config.mcpServers
|
|
35
|
+
: null;
|
|
36
|
+
if (!servers) continue;
|
|
37
|
+
|
|
38
|
+
for (const [serverName, server] of Object.entries(servers)) {
|
|
39
|
+
if (!hasBroadOpenPermissions(server)) continue;
|
|
40
|
+
const command = typeof server.command === 'string' ? server.command : '';
|
|
41
|
+
findings.push({
|
|
42
|
+
severity: isClearlyLocalMcpBinary(command) ? 'high' : 'critical',
|
|
43
|
+
file,
|
|
44
|
+
line: ctx.lineNumber(file, new RegExp(`"${escapeRegExp(serverName)}"`)) || 1,
|
|
45
|
+
fix: `MCP server "${serverName}" in ${file} has broad access without an allowlist. Add a narrow allow/permissions list before relying on it in CI or production repos.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return findings;
|
|
51
|
+
},
|
|
52
|
+
};
|