@nerviq/cli 1.29.0 → 1.30.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +1764 -1493
  2. package/README.md +568 -538
  3. package/SECURITY.md +78 -82
  4. package/bin/cli.js +2838 -2558
  5. package/docs/api-reference.md +356 -356
  6. package/docs/audit-fix.md +109 -0
  7. package/docs/autofix.md +3 -62
  8. package/docs/getting-started.md +1 -1
  9. package/docs/index.html +592 -592
  10. package/docs/integration-contracts.md +287 -287
  11. package/docs/maintenance.md +128 -128
  12. package/docs/new-platform-guide.md +202 -202
  13. package/docs/release-process.md +63 -0
  14. package/docs/shallow-risk.md +244 -244
  15. package/docs/why-nerviq.md +82 -82
  16. package/package.json +75 -67
  17. package/sdk/README.md +12 -3
  18. package/sdk/examples/langchain-integration.md +128 -0
  19. package/sdk/examples/self-governing-agent.js +135 -0
  20. package/sdk/index.d.ts +115 -0
  21. package/sdk/index.js +94 -0
  22. package/sdk/package.json +11 -0
  23. package/src/activity.js +13 -0
  24. package/src/aider/activity.js +226 -226
  25. package/src/aider/context.js +162 -162
  26. package/src/aider/freshness.js +123 -123
  27. package/src/aider/techniques.js +3465 -3465
  28. package/src/audit/layers.js +180 -180
  29. package/src/audit.js +1133 -1032
  30. package/src/auto-suggest.js +9 -2
  31. package/src/behavioral-drift.js +37 -2
  32. package/src/benchmark.js +299 -299
  33. package/src/codex/activity.js +324 -324
  34. package/src/codex/freshness.js +149 -142
  35. package/src/codex/techniques.js +4895 -4895
  36. package/src/context.js +326 -326
  37. package/src/continuous-ops.js +11 -1
  38. package/src/convert.js +340 -340
  39. package/src/copilot/config-parser.js +280 -280
  40. package/src/copilot/context.js +218 -218
  41. package/src/copilot/freshness.js +184 -177
  42. package/src/copilot/patch.js +238 -238
  43. package/src/copilot/techniques.js +3578 -3578
  44. package/src/cursor/freshness.js +194 -194
  45. package/src/cursor/patch.js +243 -243
  46. package/src/cursor/techniques.js +3735 -3735
  47. package/src/doctor.js +201 -201
  48. package/src/fix-engine.js +511 -8
  49. package/src/formatters/csv.js +86 -86
  50. package/src/formatters/junit.js +123 -123
  51. package/src/formatters/markdown.js +164 -164
  52. package/src/formatters/otel.js +151 -151
  53. package/src/freshness.js +163 -156
  54. package/src/gemini/activity.js +402 -402
  55. package/src/gemini/context.js +290 -290
  56. package/src/gemini/freshness.js +188 -188
  57. package/src/gemini/patch.js +229 -229
  58. package/src/gemini/techniques.js +3811 -3811
  59. package/src/governance.js +533 -533
  60. package/src/harmony/audit.js +306 -306
  61. package/src/i18n.js +63 -63
  62. package/src/insights.js +119 -119
  63. package/src/integrations.js +134 -134
  64. package/src/locales/en.json +33 -33
  65. package/src/locales/es.json +33 -33
  66. package/src/migrate.js +354 -354
  67. package/src/opencode/activity.js +286 -286
  68. package/src/opencode/freshness.js +137 -137
  69. package/src/opencode/techniques.js +3450 -3450
  70. package/src/safe-glyph.js +97 -0
  71. package/src/setup/analysis.js +12 -12
  72. package/src/setup.js +13 -6
  73. package/src/shallow-risk/index.js +113 -56
  74. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +51 -50
  75. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +47 -46
  76. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +47 -46
  77. package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
  78. package/src/shallow-risk/patterns/agent-config-missing-file.js +318 -317
  79. package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
  80. package/src/shallow-risk/patterns/agent-config-secret-literal.js +52 -49
  81. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +35 -34
  82. package/src/shallow-risk/patterns/hook-script-missing.js +71 -70
  83. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +53 -52
  84. package/src/shallow-risk/shared.js +653 -648
  85. package/src/source-urls.js +295 -295
  86. package/src/state-paths.js +85 -85
  87. package/src/supplemental-checks.js +805 -805
  88. package/src/telemetry.js +160 -160
  89. package/src/watch.js +46 -0
  90. package/src/windsurf/context.js +359 -359
  91. package/src/windsurf/freshness.js +194 -194
  92. package/src/windsurf/patch.js +231 -231
  93. package/src/windsurf/techniques.js +3779 -3779
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const {
5
+ SHALLOW_RISK_DOC_URL,
6
+ fileExists,
7
+ getAgentConfigEntries,
8
+ getScannableLines,
9
+ } = require('../shared');
10
+
11
+ // Match common JS package-manager script invocations:
12
+ // npm test
13
+ // npm run <name>
14
+ // pnpm <name> (pnpm <script> is shorthand for pnpm run <script>)
15
+ // pnpm run <name>
16
+ // yarn <name> (yarn <script> is shorthand)
17
+ // yarn run <name>
18
+ // bun run <name>
19
+ // bunx <name> (bunx is similar to npx — out of scope; we don't flag this)
20
+ const SCRIPT_INVOCATION_RE = /\b(npm|pnpm|yarn|bun)(?:\s+run)?\s+([A-Za-z][\w:-]*)\b/g;
21
+
22
+ // Built-in npm/yarn/pnpm/bun lifecycle scripts that don't need to exist in
23
+ // `scripts`. `npm test` will run a default echo if no `test` script exists,
24
+ // but we still flag missing `test` because agent guidance to "run npm test"
25
+ // when no script exists IS actionable repo-guidance drift.
26
+ // We exclude only commands that are truly built-in package-manager verbs
27
+ // without script-name semantics (install, ci, audit, ls, etc.).
28
+ const PACKAGE_MANAGER_BUILTINS = new Set([
29
+ 'install', 'i', 'ci', 'add', 'remove', 'rm', 'update', 'up', 'upgrade',
30
+ 'audit', 'ls', 'list', 'outdated', 'init', 'pack', 'publish', 'unpublish',
31
+ 'view', 'info', 'help', 'version', 'config', 'login', 'logout', 'whoami',
32
+ 'link', 'unlink', 'prefix', 'doctor', 'exec', 'create', 'dlx',
33
+ // Common in pnpm/yarn-only:
34
+ 'why', 'fund', 'workspace', 'workspaces', 'recursive', 'r',
35
+ // bun-only verbs:
36
+ 'add', 'remove', 'install',
37
+ ]);
38
+
39
+ function readPackageJsonScripts(ctx) {
40
+ if (ctx.__nerviqPackageJsonScripts !== undefined) {
41
+ return ctx.__nerviqPackageJsonScripts;
42
+ }
43
+ if (!fileExists(ctx, 'package.json')) {
44
+ ctx.__nerviqPackageJsonScripts = null;
45
+ return null;
46
+ }
47
+ const raw = ctx.fileContent('package.json');
48
+ if (!raw) {
49
+ ctx.__nerviqPackageJsonScripts = null;
50
+ return null;
51
+ }
52
+ try {
53
+ const pkg = JSON.parse(raw);
54
+ const scripts = (pkg && pkg.scripts && typeof pkg.scripts === 'object') ? pkg.scripts : {};
55
+ const set = new Set(Object.keys(scripts));
56
+ ctx.__nerviqPackageJsonScripts = set;
57
+ return set;
58
+ } catch {
59
+ ctx.__nerviqPackageJsonScripts = null;
60
+ return null;
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ key: 'agent-config-script-not-in-package-json',
66
+ name: 'Agent config references npm script that does not exist',
67
+ severity: 'high',
68
+ layer: 'shallow-risk',
69
+ sourceUrl: SHALLOW_RISK_DOC_URL,
70
+ owaspTags: ['agentic-top-10:tool-instruction-integrity'],
71
+ run(ctx) {
72
+ const scripts = readPackageJsonScripts(ctx);
73
+ if (!scripts) return [];
74
+
75
+ const findings = [];
76
+ const seenPerFile = new Map();
77
+
78
+ for (const entry of getAgentConfigEntries(ctx)) {
79
+ const lines = getScannableLines(entry.content);
80
+ for (const { lineNumber, text } of lines) {
81
+ SCRIPT_INVOCATION_RE.lastIndex = 0;
82
+ let match;
83
+ while ((match = SCRIPT_INVOCATION_RE.exec(text)) !== null) {
84
+ const scriptName = match[2];
85
+ if (!scriptName) continue;
86
+ if (PACKAGE_MANAGER_BUILTINS.has(scriptName.toLowerCase())) continue;
87
+ if (scripts.has(scriptName)) continue;
88
+ // Skip if the agent doc explicitly notes the script is missing
89
+ // (e.g., a corrective note like "(does NOT define `npm test`...)")
90
+ if (/\b(?:does\s+not|doesn['’]t|don['’]t)\s+(?:define|have|exist)/i.test(text)) continue;
91
+ if (/\bdo NOT define\b/i.test(text)) continue;
92
+
93
+ const dedupeKey = `${entry.path}|${scriptName}`;
94
+ if (seenPerFile.has(dedupeKey)) continue;
95
+ seenPerFile.set(dedupeKey, true);
96
+
97
+ findings.push({
98
+ file: entry.path,
99
+ line: lineNumber,
100
+ fix: `${entry.path} tells the agent to run \`${match[1]} ${scriptName === 'test' || scriptName === 'start' ? scriptName : `run ${scriptName}`}\`, but \`scripts.${scriptName}\` is not defined in package.json. Either add the script to package.json, or rewrite the agent guidance to reflect what actually exists.`,
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ return findings;
107
+ },
108
+ };
@@ -1,49 +1,52 @@
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
+ // POS-01a: machine-readable OWASP cross-walk tags. See
24
+ // research/pos-01-owasp-vocabulary-mapping-2026-04-28.md.
25
+ owaspTags: ['agentic-top-10:insecure-agent-instructions', 'mcp-top-10:credential-leak'],
26
+ run(ctx) {
27
+ const findings = [];
28
+
29
+ for (const entry of getAgentConfigEntries(ctx)) {
30
+ const lines = entry.content.split(/\r?\n/);
31
+ for (let index = 0; index < lines.length; index++) {
32
+ const line = lines[index];
33
+ for (const secret of SECRET_PATTERNS) {
34
+ secret.pattern.lastIndex = 0;
35
+ let match = secret.pattern.exec(line);
36
+ while (match) {
37
+ if (!looksLikePlaceholder(line, match[0])) {
38
+ findings.push({
39
+ file: entry.path,
40
+ line: index + 1,
41
+ 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.`,
42
+ });
43
+ }
44
+ match = secret.pattern.exec(line);
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ return findings;
51
+ },
52
+ };
@@ -1,34 +1,35 @@
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
+ owaspTags: ['agentic-top-10:tool-instruction-integrity'],
17
+ run(ctx) {
18
+ const claims = collectStackClaims(ctx);
19
+ const distinctClaims = [...new Set(claims.map((claim) => claim.key))];
20
+ if (distinctClaims.length !== 1) return [];
21
+
22
+ const declared = claims[0];
23
+ const hasDeclaredEvidence = declared.stackKeys.some((stackKey) => Boolean(findFirstStackEvidence(ctx, stackKey)));
24
+ if (hasDeclaredEvidence) return [];
25
+
26
+ const actual = getDetectedStackEvidence(ctx).find((item) => !declared.stackKeys.includes(item.key));
27
+ if (!actual) return [];
28
+
29
+ return [{
30
+ file: declared.file,
31
+ line: declared.line,
32
+ 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.`,
33
+ }];
34
+ },
35
+ };
@@ -1,70 +1,71 @@
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
+ owaspTags: ['agentic-skills-top-10:skill-supply-chain', 'agentic-top-10:tool-instruction-integrity'],
46
+ run(ctx) {
47
+ const file = '.claude/settings.json';
48
+ const config = ctx.jsonFile(file);
49
+ if (!config || !config.hooks || typeof config.hooks !== 'object') {
50
+ return [];
51
+ }
52
+
53
+ const findings = [];
54
+ for (const [eventName, entries] of Object.entries(config.hooks)) {
55
+ if (!HOOK_EVENTS.has(eventName)) continue;
56
+ for (const command of collectHookCommands(entries)) {
57
+ const scriptPath = getHookCommandPath(command);
58
+ if (!scriptPath) continue;
59
+ const resolvedPath = resolveRepoPath(ctx, file, scriptPath, 'repo-root');
60
+ if (!resolvedPath || ctx.fileContent(resolvedPath) !== null) continue;
61
+ findings.push({
62
+ file,
63
+ line: ctx.lineNumber(file, new RegExp(escapeRegExp(command))) || ctx.lineNumber(file, new RegExp(`"${escapeRegExp(eventName)}"`)) || 1,
64
+ fix: `${file} declares a ${eventName} hook at \`${resolvedPath}\`, but the script is missing. Create the hook file or remove the dead hook reference.`,
65
+ });
66
+ }
67
+ }
68
+
69
+ return findings;
70
+ },
71
+ };
@@ -1,52 +1,53 @@
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
+ owaspTags: ['mcp-top-10:server-allowlist', 'mcp-top-10:tool-poisoning', 'agentic-top-10:excessive-agency'],
28
+ run(ctx) {
29
+ const findings = [];
30
+ const candidates = ['.claude/settings.json', '.mcp.json'];
31
+
32
+ for (const file of candidates) {
33
+ const config = ctx.jsonFile(file);
34
+ const servers = config && config.mcpServers && typeof config.mcpServers === 'object'
35
+ ? config.mcpServers
36
+ : null;
37
+ if (!servers) continue;
38
+
39
+ for (const [serverName, server] of Object.entries(servers)) {
40
+ if (!hasBroadOpenPermissions(server)) continue;
41
+ const command = typeof server.command === 'string' ? server.command : '';
42
+ findings.push({
43
+ severity: isClearlyLocalMcpBinary(command) ? 'high' : 'critical',
44
+ file,
45
+ line: ctx.lineNumber(file, new RegExp(`"${escapeRegExp(serverName)}"`)) || 1,
46
+ 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.`,
47
+ });
48
+ }
49
+ }
50
+
51
+ return findings;
52
+ },
53
+ };