@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.
- package/CHANGELOG.md +1764 -1493
- package/README.md +568 -538
- package/SECURITY.md +78 -82
- package/bin/cli.js +2838 -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 +75 -67
- package/sdk/README.md +12 -3
- package/sdk/examples/langchain-integration.md +128 -0
- package/sdk/examples/self-governing-agent.js +135 -0
- package/sdk/index.d.ts +115 -0
- package/sdk/index.js +94 -0
- package/sdk/package.json +11 -0
- package/src/activity.js +13 -0
- 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 +1133 -1032
- package/src/auto-suggest.js +9 -2
- package/src/behavioral-drift.js +37 -2
- package/src/benchmark.js +299 -299
- package/src/codex/activity.js +324 -324
- package/src/codex/freshness.js +149 -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 +184 -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 +163 -156
- package/src/gemini/activity.js +402 -402
- package/src/gemini/context.js +290 -290
- package/src/gemini/freshness.js +188 -188
- 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/safe-glyph.js +97 -0
- package/src/setup/analysis.js +12 -12
- package/src/setup.js +13 -6
- package/src/shallow-risk/index.js +113 -56
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +51 -50
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +47 -46
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +47 -46
- package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
- package/src/shallow-risk/patterns/agent-config-missing-file.js +318 -317
- package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +52 -49
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +35 -34
- package/src/shallow-risk/patterns/hook-script-missing.js +71 -70
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +53 -52
- package/src/shallow-risk/shared.js +653 -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/watch.js +46 -0
- 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
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
};
|