@nerviq/cli 1.10.0 → 1.12.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/README.md +176 -47
- package/bin/cli.js +842 -287
- package/package.json +2 -2
- package/src/activity.js +225 -59
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +28 -25
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +131 -1
- package/src/anti-patterns.js +17 -2
- package/src/audit.js +197 -96
- package/src/behavioral-drift.js +801 -0
- package/src/benchmark.js +15 -10
- package/src/continuous-ops.js +681 -0
- package/src/cost-tracking.js +61 -0
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/governance.js +72 -50
- package/src/hook-validation.js +342 -0
- package/src/index.js +7 -1
- package/src/integrations.js +144 -60
- package/src/mcp-validation.js +337 -0
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/permission-rules.js +218 -0
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +36 -2
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5611
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/watch.js +18 -0
- package/src/windsurf/techniques.js +17 -12
- package/src/workspace.js +105 -8
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security technique fragments.
|
|
3
|
+
* Generated mechanically from the legacy techniques.js monolith during HR-09.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
collectClaudeDenyRules,
|
|
8
|
+
hasPromptInjectionDefenseGuidance,
|
|
9
|
+
hasMcpPromptInjectionDefenseGuidance,
|
|
10
|
+
hasInjectionDefenseHookConfigured,
|
|
11
|
+
getRepoInstructionBundle,
|
|
12
|
+
getWorkflowContent,
|
|
13
|
+
} = require('./shared');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
settingsPermissions: {
|
|
17
|
+
id: 24,
|
|
18
|
+
name: 'Permission configuration',
|
|
19
|
+
check: (ctx) => {
|
|
20
|
+
// Prefer local (effective config) — any settings file with permissions passes
|
|
21
|
+
const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
|
|
22
|
+
return !!(settings && settings.permissions);
|
|
23
|
+
},
|
|
24
|
+
impact: 'medium',
|
|
25
|
+
rating: 4,
|
|
26
|
+
category: 'security',
|
|
27
|
+
fix: 'Configure allow/deny permission lists for safe tool usage.',
|
|
28
|
+
template: null
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
permissionDeny: {
|
|
32
|
+
id: 2401,
|
|
33
|
+
name: 'Deny rules configured in permissions',
|
|
34
|
+
check: (ctx) => {
|
|
35
|
+
return collectClaudeDenyRules(ctx).length > 0;
|
|
36
|
+
},
|
|
37
|
+
impact: 'high',
|
|
38
|
+
rating: 5,
|
|
39
|
+
category: 'security',
|
|
40
|
+
fix: 'Add permissions.deny rules to block dangerous operations (e.g. rm -rf, dropping databases).',
|
|
41
|
+
template: null
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
noBypassPermissions: {
|
|
45
|
+
id: 2402,
|
|
46
|
+
name: 'Default mode is not bypassPermissions',
|
|
47
|
+
check: (ctx) => {
|
|
48
|
+
// Check shared settings first (committed to git) — if the shared baseline
|
|
49
|
+
// is safe, a personal settings.local.json override should not fail the audit.
|
|
50
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
51
|
+
if (shared && shared.permissions) {
|
|
52
|
+
return shared.permissions.defaultMode !== 'bypassPermissions';
|
|
53
|
+
}
|
|
54
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
55
|
+
if (!local || !local.permissions) return null;
|
|
56
|
+
return local.permissions.defaultMode !== 'bypassPermissions';
|
|
57
|
+
},
|
|
58
|
+
impact: 'critical',
|
|
59
|
+
rating: 5,
|
|
60
|
+
category: 'security',
|
|
61
|
+
fix: 'Do not set defaultMode to bypassPermissions. Use explicit allow rules instead.',
|
|
62
|
+
template: null
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
secretsProtection: {
|
|
66
|
+
id: 1096,
|
|
67
|
+
name: 'Secrets protection configured',
|
|
68
|
+
check: (ctx) => {
|
|
69
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
70
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
71
|
+
const settings = shared || local;
|
|
72
|
+
if (!settings || !settings.permissions) return false;
|
|
73
|
+
const denyRules = collectClaudeDenyRules(ctx);
|
|
74
|
+
const hasDeny = denyRules.some((rule) => rule.protectsSecrets);
|
|
75
|
+
// Fail if allow includes "*" (overly broad — bypasses deny rules)
|
|
76
|
+
const allow = settings.permissions.allow || [];
|
|
77
|
+
if (Array.isArray(allow) && allow.includes('*')) return false;
|
|
78
|
+
return hasDeny;
|
|
79
|
+
},
|
|
80
|
+
impact: 'critical',
|
|
81
|
+
rating: 5,
|
|
82
|
+
category: 'security',
|
|
83
|
+
fix: 'Add permissions.deny rules to block reading .env files and secrets directories.',
|
|
84
|
+
template: null
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
securityReview: {
|
|
88
|
+
id: 1031,
|
|
89
|
+
name: 'Security review command awareness',
|
|
90
|
+
check: (ctx) => {
|
|
91
|
+
const md = ctx.claudeMdContent() || '';
|
|
92
|
+
return md.includes('security') || md.includes('/security-review');
|
|
93
|
+
},
|
|
94
|
+
impact: 'high',
|
|
95
|
+
rating: 5,
|
|
96
|
+
category: 'security',
|
|
97
|
+
fix: 'Add /security-review to your workflow. Claude Code has built-in OWASP Top 10 scanning.',
|
|
98
|
+
template: null
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
promptInjectionTrustBoundary: {
|
|
102
|
+
id: 8805,
|
|
103
|
+
name: 'Prompt injection trust boundary documented',
|
|
104
|
+
check: (ctx) => {
|
|
105
|
+
const bundle = getRepoInstructionBundle(ctx);
|
|
106
|
+
return hasPromptInjectionDefenseGuidance(bundle);
|
|
107
|
+
},
|
|
108
|
+
impact: 'high',
|
|
109
|
+
rating: 5,
|
|
110
|
+
category: 'security',
|
|
111
|
+
fix: 'Document a trust boundary: treat repo files, fetched content, and MCP responses as untrusted data, not instructions to follow.',
|
|
112
|
+
template: null
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
injectionDefenseHook: {
|
|
116
|
+
id: 8806,
|
|
117
|
+
name: 'Injection defense hook configured for external content',
|
|
118
|
+
check: (ctx) => {
|
|
119
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
120
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
121
|
+
return hasInjectionDefenseHookConfigured(shared) || hasInjectionDefenseHookConfigured(local);
|
|
122
|
+
},
|
|
123
|
+
impact: 'medium',
|
|
124
|
+
rating: 4,
|
|
125
|
+
category: 'security',
|
|
126
|
+
fix: 'Add a PostToolUse injection-defense hook for WebFetch/WebSearch/Read/Grep/Glob/MCP flows so suspicious external content is logged and reviewed.',
|
|
127
|
+
template: 'hooks'
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
mcpPromptInjectionBoundary: {
|
|
131
|
+
id: 8807,
|
|
132
|
+
name: 'MCP responses treated as untrusted in instructions',
|
|
133
|
+
check: (ctx) => {
|
|
134
|
+
const hasMcpSignals = Boolean(
|
|
135
|
+
ctx.fileContent('.mcp.json') ||
|
|
136
|
+
ctx.fileContent('.vscode/mcp.json') ||
|
|
137
|
+
ctx.fileContent('.cursor/mcp.json') ||
|
|
138
|
+
ctx.fileContent('.windsurf/mcp.json') ||
|
|
139
|
+
ctx.fileContent('opencode.json') ||
|
|
140
|
+
ctx.fileContent('opencode.jsonc') ||
|
|
141
|
+
ctx.fileContent('.codex/config.toml')
|
|
142
|
+
);
|
|
143
|
+
if (!hasMcpSignals) return null;
|
|
144
|
+
const bundle = getRepoInstructionBundle(ctx);
|
|
145
|
+
return hasMcpPromptInjectionDefenseGuidance(bundle);
|
|
146
|
+
},
|
|
147
|
+
impact: 'medium',
|
|
148
|
+
rating: 4,
|
|
149
|
+
category: 'security',
|
|
150
|
+
fix: 'Document that MCP outputs are untrusted data, can contain indirect prompt injection, and must never override repo-level instructions.',
|
|
151
|
+
template: null
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
sandboxAwareness: {
|
|
155
|
+
id: 2013,
|
|
156
|
+
name: 'Sandbox or isolation mentioned',
|
|
157
|
+
check: (ctx) => {
|
|
158
|
+
const md = ctx.claudeMdContent() || '';
|
|
159
|
+
const settings = ctx.jsonFile('.claude/settings.json') || {};
|
|
160
|
+
return /sandbox|isolat/i.test(md) || !!settings.sandbox;
|
|
161
|
+
},
|
|
162
|
+
impact: 'medium', rating: 3, category: 'security',
|
|
163
|
+
fix: 'Claude Code supports sandboxed command execution. Consider enabling it for untrusted operations.',
|
|
164
|
+
template: null
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
denyRulesDepth: {
|
|
168
|
+
id: 2014,
|
|
169
|
+
name: 'Deny rules cover 3+ patterns',
|
|
170
|
+
check: (ctx) => {
|
|
171
|
+
return collectClaudeDenyRules(ctx).length >= 3;
|
|
172
|
+
},
|
|
173
|
+
impact: 'high', rating: 4, category: 'security',
|
|
174
|
+
fix: 'Add at least 3 deny rules: rm -rf, force-push, and .env reads. More patterns = safer Claude.',
|
|
175
|
+
template: null
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
sbomExists: {
|
|
179
|
+
id: 130041,
|
|
180
|
+
name: 'SBOM file exists',
|
|
181
|
+
check: (ctx) => {
|
|
182
|
+
return ctx.files.some(f => /sbom\.(json|xml|cdx\.json)|bom\.xml|cyclonedx/i.test(f));
|
|
183
|
+
},
|
|
184
|
+
impact: 'medium',
|
|
185
|
+
category: 'supply-chain',
|
|
186
|
+
fix: 'Generate an SBOM (Software Bill of Materials) in CycloneDX or SPDX format for supply chain transparency.',
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
dependencyPinning: {
|
|
190
|
+
id: 130042,
|
|
191
|
+
name: 'Lock files committed',
|
|
192
|
+
check: (ctx) => {
|
|
193
|
+
return ctx.files.some(f => /^(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|poetry\.lock|Pipfile\.lock|bun\.lockb|composer\.lock|Gemfile\.lock|go\.sum)$/i.test(f));
|
|
194
|
+
},
|
|
195
|
+
impact: 'high',
|
|
196
|
+
category: 'supply-chain',
|
|
197
|
+
fix: 'Commit lock files (package-lock.json, yarn.lock, Cargo.lock, poetry.lock) for reproducible builds.',
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
provenanceAttestation: {
|
|
201
|
+
id: 130043,
|
|
202
|
+
name: 'Provenance or sigstore in CI',
|
|
203
|
+
check: (ctx) => {
|
|
204
|
+
const ci = getWorkflowContent(ctx);
|
|
205
|
+
return /provenance|sigstore|cosign|slsa|attestation/i.test(ci);
|
|
206
|
+
},
|
|
207
|
+
impact: 'medium',
|
|
208
|
+
category: 'supply-chain',
|
|
209
|
+
fix: 'Add npm provenance or sigstore attestation in CI to verify package integrity.',
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
lockfileIntegrity: {
|
|
213
|
+
id: 130044,
|
|
214
|
+
name: 'CI uses frozen lockfile install',
|
|
215
|
+
check: (ctx) => {
|
|
216
|
+
const ci = getWorkflowContent(ctx);
|
|
217
|
+
return /npm ci\b|--frozen-lockfile|--immutable|cargo.*--locked|pip install.*--require-hashes/i.test(ci);
|
|
218
|
+
},
|
|
219
|
+
impact: 'high',
|
|
220
|
+
category: 'supply-chain',
|
|
221
|
+
fix: 'Use `npm ci` or `--frozen-lockfile` in CI instead of `npm install` for deterministic builds.',
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
dependencyScanning: {
|
|
225
|
+
id: 130045,
|
|
226
|
+
name: 'Dependency scanning configured',
|
|
227
|
+
check: (ctx) => {
|
|
228
|
+
const hasConfig = ctx.files.some(f => /dependabot\.yml|renovate\.json|\.snyk/i.test(f));
|
|
229
|
+
if (hasConfig) return true;
|
|
230
|
+
const ci = getWorkflowContent(ctx);
|
|
231
|
+
return /dependabot|renovate|snyk|npm audit|cargo audit|pip-audit|safety check/i.test(ci);
|
|
232
|
+
},
|
|
233
|
+
impact: 'high',
|
|
234
|
+
category: 'supply-chain',
|
|
235
|
+
fix: 'Configure Dependabot, Renovate, or Snyk to automatically scan and update vulnerable dependencies.',
|
|
236
|
+
},
|
|
237
|
+
};
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Claude technique modules.
|
|
3
|
+
* Generated mechanically from the legacy monolith during HR-09.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { collectClaudeDenyRules } = require('../permission-rules');
|
|
9
|
+
const {
|
|
10
|
+
hasPromptInjectionDefenseGuidance,
|
|
11
|
+
hasMcpPromptInjectionDefenseGuidance,
|
|
12
|
+
hasInjectionDefenseHookConfigured,
|
|
13
|
+
} = require('../prompt-injection');
|
|
14
|
+
const {
|
|
15
|
+
getClaudeInstructionBundle,
|
|
16
|
+
getRepoInstructionBundle,
|
|
17
|
+
hasDocumentedVerificationGuidance,
|
|
18
|
+
hasDocumentedTestCommand,
|
|
19
|
+
hasDocumentedLintCommand,
|
|
20
|
+
hasDocumentedBuildCommand,
|
|
21
|
+
} = require('../instruction-surfaces');
|
|
22
|
+
|
|
23
|
+
function hasFrontendSignals(ctx) {
|
|
24
|
+
const pkg = ctx.fileContent('package.json') || '';
|
|
25
|
+
return /react|vue|angular|next|svelte|tailwind|vite|astro/i.test(pkg) ||
|
|
26
|
+
ctx.files.some(f => /tailwind\.config|vite\.config|next\.config|svelte\.config|nuxt\.config|pages\/|components\/|app\//i.test(f));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getClaudeHookContents(ctx) {
|
|
30
|
+
const hookFiles = ctx.dirFiles('.claude/hooks').filter(f => /\.(js|cjs|mjs|ts|sh|py)$/.test(f));
|
|
31
|
+
return hookFiles.map(f => ctx.fileContent(`.claude/hooks/${f}`) || '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchesPattern(value, pattern) {
|
|
35
|
+
if (pattern instanceof RegExp) {
|
|
36
|
+
pattern.lastIndex = 0;
|
|
37
|
+
return pattern.test(value);
|
|
38
|
+
}
|
|
39
|
+
return value === pattern;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getProjectEntries(ctx) {
|
|
43
|
+
if (ctx.__nerviqProjectEntries) return ctx.__nerviqProjectEntries;
|
|
44
|
+
|
|
45
|
+
const entries = [];
|
|
46
|
+
const skippedDirs = new Set([
|
|
47
|
+
'.git',
|
|
48
|
+
'.hg',
|
|
49
|
+
'.svn',
|
|
50
|
+
'node_modules',
|
|
51
|
+
'__pycache__',
|
|
52
|
+
'.pytest_cache',
|
|
53
|
+
'.mypy_cache',
|
|
54
|
+
'.ruff_cache',
|
|
55
|
+
'.venv',
|
|
56
|
+
'venv',
|
|
57
|
+
'env',
|
|
58
|
+
'.tox',
|
|
59
|
+
'.nox',
|
|
60
|
+
'vendor',
|
|
61
|
+
'dist',
|
|
62
|
+
'build',
|
|
63
|
+
'coverage',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const walk = (relPath = '') => {
|
|
67
|
+
const fullPath = relPath
|
|
68
|
+
? path.join(ctx.dir, ...relPath.split('/'))
|
|
69
|
+
: ctx.dir;
|
|
70
|
+
|
|
71
|
+
let dirents = [];
|
|
72
|
+
try {
|
|
73
|
+
dirents = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const dirent of dirents) {
|
|
79
|
+
if (dirent.name === '.DS_Store') continue;
|
|
80
|
+
|
|
81
|
+
const entryPath = relPath ? `${relPath}/${dirent.name}` : dirent.name;
|
|
82
|
+
if (dirent.isDirectory()) {
|
|
83
|
+
if (skippedDirs.has(dirent.name)) continue;
|
|
84
|
+
entries.push(`${entryPath}/`);
|
|
85
|
+
walk(entryPath);
|
|
86
|
+
} else {
|
|
87
|
+
entries.push(entryPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
walk();
|
|
93
|
+
ctx.__nerviqProjectEntries = entries;
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getProjectFiles(ctx) {
|
|
98
|
+
if (ctx.__nerviqProjectFiles) return ctx.__nerviqProjectFiles;
|
|
99
|
+
ctx.__nerviqProjectFiles = getProjectEntries(ctx).filter(entry => !entry.endsWith('/'));
|
|
100
|
+
return ctx.__nerviqProjectFiles;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findProjectFiles(ctx, pattern) {
|
|
104
|
+
return getProjectFiles(ctx).filter(file => matchesPattern(file, pattern));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasProjectFile(ctx, pattern) {
|
|
108
|
+
return findProjectFiles(ctx, pattern).length > 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a stack-indicator file exists at a "core" location (root, src/, lib/, app/, packages/)
|
|
113
|
+
* rather than inside examples/, docs/, test/, vendor/, or deeply nested paths.
|
|
114
|
+
* This prevents false stack detection from example/demo code.
|
|
115
|
+
*/
|
|
116
|
+
const EXCLUDED_STACK_DIRS = /^(examples?|docs?|test|tests|fixtures?|samples?|demo|vendor|third[_-]?party|\.github)\//i;
|
|
117
|
+
|
|
118
|
+
function hasCoreProjectFile(ctx, pattern) {
|
|
119
|
+
return findProjectFiles(ctx, pattern).some(file => !EXCLUDED_STACK_DIRS.test(file));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasCoreRootFile(ctx, pattern) {
|
|
123
|
+
// Only match files at project root (no / in path) or one level deep (src/X, lib/X, app/X)
|
|
124
|
+
return findProjectFiles(ctx, pattern).some(file => {
|
|
125
|
+
if (EXCLUDED_STACK_DIRS.test(file)) return false;
|
|
126
|
+
const depth = (file.match(/\//g) || []).length;
|
|
127
|
+
return depth <= 1;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readProjectFiles(ctx, pattern, limit = 60) {
|
|
132
|
+
return findProjectFiles(ctx, pattern)
|
|
133
|
+
.slice(0, limit)
|
|
134
|
+
.map(file => ctx.fileContent(file) || '')
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isPythonProject(ctx) {
|
|
140
|
+
if (ctx.__nerviqIsPython !== undefined) return ctx.__nerviqIsPython;
|
|
141
|
+
// Require a Python config file (pyproject.toml, requirements.txt, setup.py) at a core location.
|
|
142
|
+
// Stray .py files in examples/ or docs/ don't make it a Python project.
|
|
143
|
+
ctx.__nerviqIsPython =
|
|
144
|
+
hasCoreRootFile(ctx, /(^|\/)(pyproject\.toml|setup\.py|Pipfile)$/i) ||
|
|
145
|
+
hasCoreRootFile(ctx, /(^|\/)requirements\.txt$/i);
|
|
146
|
+
return ctx.__nerviqIsPython;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isGoProject(ctx) {
|
|
150
|
+
if (ctx.__nerviqIsGo !== undefined) return ctx.__nerviqIsGo;
|
|
151
|
+
ctx.__nerviqIsGo = hasCoreRootFile(ctx, /(^|\/)go\.mod$/i);
|
|
152
|
+
return ctx.__nerviqIsGo;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isRustProject(ctx) {
|
|
156
|
+
if (ctx.__nerviqIsRust !== undefined) return ctx.__nerviqIsRust;
|
|
157
|
+
ctx.__nerviqIsRust = hasCoreRootFile(ctx, /(^|\/)Cargo\.toml$/i);
|
|
158
|
+
return ctx.__nerviqIsRust;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isJavaProject(ctx) {
|
|
162
|
+
if (ctx.__nerviqIsJava !== undefined) return ctx.__nerviqIsJava;
|
|
163
|
+
ctx.__nerviqIsJava = hasCoreRootFile(ctx, /(^|\/)(pom\.xml|build\.gradle|build\.gradle\.kts)$/i);
|
|
164
|
+
return ctx.__nerviqIsJava;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isFlutterProject(ctx) {
|
|
168
|
+
if (ctx.__nerviqIsFlutter !== undefined) return ctx.__nerviqIsFlutter;
|
|
169
|
+
ctx.__nerviqIsFlutter = hasCoreRootFile(ctx, /(^|\/)pubspec\.yaml$/i);
|
|
170
|
+
return ctx.__nerviqIsFlutter;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isSwiftProject(ctx) {
|
|
174
|
+
if (ctx.__nerviqIsSwift !== undefined) return ctx.__nerviqIsSwift;
|
|
175
|
+
ctx.__nerviqIsSwift = hasCoreRootFile(ctx, /(^|\/)Package\.swift$/i) ||
|
|
176
|
+
hasCoreProjectFile(ctx, /\.xcodeproj/i);
|
|
177
|
+
return ctx.__nerviqIsSwift;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isKotlinProject(ctx) {
|
|
181
|
+
if (ctx.__nerviqIsKotlin !== undefined) return ctx.__nerviqIsKotlin;
|
|
182
|
+
const gradle = (ctx.fileContent('build.gradle.kts') || '') + (ctx.fileContent('build.gradle') || '');
|
|
183
|
+
ctx.__nerviqIsKotlin = /kotlin/i.test(gradle);
|
|
184
|
+
return ctx.__nerviqIsKotlin;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isRubyProject(ctx) {
|
|
188
|
+
if (ctx.__nerviqIsRuby !== undefined) return ctx.__nerviqIsRuby;
|
|
189
|
+
ctx.__nerviqIsRuby = hasCoreRootFile(ctx, /(^|\/)Gemfile$/i);
|
|
190
|
+
return ctx.__nerviqIsRuby;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isPhpProject(ctx) {
|
|
194
|
+
if (ctx.__nerviqIsPhp !== undefined) return ctx.__nerviqIsPhp;
|
|
195
|
+
ctx.__nerviqIsPhp = hasCoreRootFile(ctx, /(^|\/)composer\.json$/i);
|
|
196
|
+
return ctx.__nerviqIsPhp;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isDotnetProject(ctx) {
|
|
200
|
+
if (ctx.__nerviqIsDotnet !== undefined) return ctx.__nerviqIsDotnet;
|
|
201
|
+
ctx.__nerviqIsDotnet = hasCoreProjectFile(ctx, /(^|\/)(.*\.csproj|.*\.sln|global\.json)$/i);
|
|
202
|
+
return ctx.__nerviqIsDotnet;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Map category names to their project detection function.
|
|
207
|
+
* Used by the audit to skip entire categories when the stack isn't detected.
|
|
208
|
+
*/
|
|
209
|
+
const STACK_CATEGORY_DETECTORS = {
|
|
210
|
+
python: isPythonProject,
|
|
211
|
+
go: isGoProject,
|
|
212
|
+
rust: isRustProject,
|
|
213
|
+
java: isJavaProject,
|
|
214
|
+
flutter: isFlutterProject,
|
|
215
|
+
swift: isSwiftProject,
|
|
216
|
+
kotlin: isKotlinProject,
|
|
217
|
+
ruby: isRubyProject,
|
|
218
|
+
php: isPhpProject,
|
|
219
|
+
dotnet: isDotnetProject,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function getPythonFiles(ctx) {
|
|
223
|
+
if (ctx.__nerviqPythonFiles) return ctx.__nerviqPythonFiles;
|
|
224
|
+
ctx.__nerviqPythonFiles = findProjectFiles(ctx, /\.py$/i);
|
|
225
|
+
return ctx.__nerviqPythonFiles;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getMainPythonFiles(ctx) {
|
|
229
|
+
if (ctx.__nerviqMainPythonFiles) return ctx.__nerviqMainPythonFiles;
|
|
230
|
+
ctx.__nerviqMainPythonFiles = getPythonFiles(ctx)
|
|
231
|
+
.filter(file => !/(^|\/)(tests?|__pycache__|migrations)\//i.test(file))
|
|
232
|
+
.filter(file => !/(^|\/)(test_[^/]+|conftest)\.py$/i.test(file))
|
|
233
|
+
.slice(0, 50);
|
|
234
|
+
return ctx.__nerviqMainPythonFiles;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getPythonProjectText(ctx) {
|
|
238
|
+
if (ctx.__nerviqPythonProjectText) return ctx.__nerviqPythonProjectText;
|
|
239
|
+
ctx.__nerviqPythonProjectText = [
|
|
240
|
+
readProjectFiles(ctx, /(^|\/)pyproject\.toml$/i),
|
|
241
|
+
readProjectFiles(ctx, /(^|\/)requirements[^/]*\.txt$/i),
|
|
242
|
+
readProjectFiles(ctx, /(^|\/)setup\.py$/i),
|
|
243
|
+
readProjectFiles(ctx, /(^|\/)setup\.cfg$/i),
|
|
244
|
+
readProjectFiles(ctx, /(^|\/)Pipfile$/i),
|
|
245
|
+
].filter(Boolean).join('\n');
|
|
246
|
+
return ctx.__nerviqPythonProjectText;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getGoFiles(ctx) {
|
|
250
|
+
if (ctx.__nerviqGoFiles) return ctx.__nerviqGoFiles;
|
|
251
|
+
ctx.__nerviqGoFiles = findProjectFiles(ctx, /\.go$/i);
|
|
252
|
+
return ctx.__nerviqGoFiles;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getRustFiles(ctx) {
|
|
256
|
+
if (ctx.__nerviqRustFiles) return ctx.__nerviqRustFiles;
|
|
257
|
+
ctx.__nerviqRustFiles = findProjectFiles(ctx, /\.rs$/i);
|
|
258
|
+
return ctx.__nerviqRustFiles;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getMainRustFiles(ctx) {
|
|
262
|
+
if (ctx.__nerviqMainRustFiles) return ctx.__nerviqMainRustFiles;
|
|
263
|
+
ctx.__nerviqMainRustFiles = getRustFiles(ctx)
|
|
264
|
+
.filter(file => !/(^|\/)(tests|target)\//i.test(file))
|
|
265
|
+
.slice(0, 60);
|
|
266
|
+
return ctx.__nerviqMainRustFiles;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getJavaFiles(ctx) {
|
|
270
|
+
if (ctx.__nerviqJavaFiles) return ctx.__nerviqJavaFiles;
|
|
271
|
+
ctx.__nerviqJavaFiles = findProjectFiles(ctx, /\.java$/i);
|
|
272
|
+
return ctx.__nerviqJavaFiles;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getMainJavaFiles(ctx) {
|
|
276
|
+
if (ctx.__nerviqMainJavaFiles) return ctx.__nerviqMainJavaFiles;
|
|
277
|
+
ctx.__nerviqMainJavaFiles = getJavaFiles(ctx)
|
|
278
|
+
.filter(file => !/(^|\/)(test|tests|src\/test)\//i.test(file))
|
|
279
|
+
.slice(0, 60);
|
|
280
|
+
return ctx.__nerviqMainJavaFiles;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function getMainGoFiles(ctx) {
|
|
284
|
+
if (ctx.__nerviqMainGoFiles) return ctx.__nerviqMainGoFiles;
|
|
285
|
+
ctx.__nerviqMainGoFiles = getGoFiles(ctx).filter(file => !/_test\.go$/i.test(file)).slice(0, 60);
|
|
286
|
+
return ctx.__nerviqMainGoFiles;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getWorkflowContent(ctx) {
|
|
290
|
+
if (ctx.__nerviqWorkflowContent !== undefined) return ctx.__nerviqWorkflowContent;
|
|
291
|
+
ctx.__nerviqWorkflowContent = readProjectFiles(ctx, /^\.github\/workflows\/.*\.ya?ml$/i);
|
|
292
|
+
return ctx.__nerviqWorkflowContent;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getPreCommitContent(ctx) {
|
|
296
|
+
if (ctx.__nerviqPreCommitContent !== undefined) return ctx.__nerviqPreCommitContent;
|
|
297
|
+
ctx.__nerviqPreCommitContent = readProjectFiles(ctx, /(^|\/)\.pre-commit-config\.ya?ml$/i);
|
|
298
|
+
return ctx.__nerviqPreCommitContent;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getGoProjectText(ctx) {
|
|
302
|
+
if (ctx.__nerviqGoProjectText) return ctx.__nerviqGoProjectText;
|
|
303
|
+
ctx.__nerviqGoProjectText = [
|
|
304
|
+
readProjectFiles(ctx, /(^|\/)go\.mod$/i),
|
|
305
|
+
getWorkflowContent(ctx),
|
|
306
|
+
readProjectFiles(ctx, /(^|\/)Makefile$/),
|
|
307
|
+
getPreCommitContent(ctx),
|
|
308
|
+
getMainGoFiles(ctx).slice(0, 25).map(file => ctx.fileContent(file) || '').filter(Boolean).join('\n'),
|
|
309
|
+
].filter(Boolean).join('\n');
|
|
310
|
+
return ctx.__nerviqGoProjectText;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getRustProjectText(ctx) {
|
|
314
|
+
if (ctx.__nerviqRustProjectText) return ctx.__nerviqRustProjectText;
|
|
315
|
+
ctx.__nerviqRustProjectText = [
|
|
316
|
+
readProjectFiles(ctx, /(^|\/)Cargo\.toml$/i),
|
|
317
|
+
readProjectFiles(ctx, /(^|\/)(clippy\.toml|\.clippy\.toml|rustfmt\.toml|\.rustfmt\.toml|build\.rs)$/i),
|
|
318
|
+
readProjectFiles(ctx, /(^|\/)\.cargo\/config\.toml$/i),
|
|
319
|
+
getWorkflowContent(ctx),
|
|
320
|
+
getMainRustFiles(ctx).slice(0, 30).map(file => ctx.fileContent(file) || '').filter(Boolean).join('\n'),
|
|
321
|
+
].filter(Boolean).join('\n');
|
|
322
|
+
return ctx.__nerviqRustProjectText;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getJavaBuildText(ctx) {
|
|
326
|
+
if (ctx.__nerviqJavaBuildText) return ctx.__nerviqJavaBuildText;
|
|
327
|
+
ctx.__nerviqJavaBuildText = [
|
|
328
|
+
readProjectFiles(ctx, /(^|\/)pom\.xml$/i),
|
|
329
|
+
readProjectFiles(ctx, /(^|\/)build\.gradle$/i),
|
|
330
|
+
readProjectFiles(ctx, /(^|\/)build\.gradle\.kts$/i),
|
|
331
|
+
readProjectFiles(ctx, /(^|\/)settings\.gradle$/i),
|
|
332
|
+
readProjectFiles(ctx, /(^|\/)settings\.gradle\.kts$/i),
|
|
333
|
+
].filter(Boolean).join('\n');
|
|
334
|
+
return ctx.__nerviqJavaBuildText;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getJavaProjectText(ctx) {
|
|
338
|
+
if (ctx.__nerviqJavaProjectText) return ctx.__nerviqJavaProjectText;
|
|
339
|
+
ctx.__nerviqJavaProjectText = [
|
|
340
|
+
getJavaBuildText(ctx),
|
|
341
|
+
getWorkflowContent(ctx),
|
|
342
|
+
readProjectFiles(ctx, /(^|\/)\.editorconfig$/i),
|
|
343
|
+
readProjectFiles(ctx, /(^|\/)(application\.properties|application\.ya?ml|logback.*\.xml|log4j2?.*\.xml)$/i),
|
|
344
|
+
getMainJavaFiles(ctx).slice(0, 30).map(file => ctx.fileContent(file) || '').filter(Boolean).join('\n'),
|
|
345
|
+
].filter(Boolean).join('\n');
|
|
346
|
+
return ctx.__nerviqJavaProjectText;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getGoInterfaceBlocks(ctx) {
|
|
350
|
+
if (ctx.__nerviqGoInterfaces) return ctx.__nerviqGoInterfaces;
|
|
351
|
+
const blocks = [];
|
|
352
|
+
for (const file of getMainGoFiles(ctx)) {
|
|
353
|
+
const content = ctx.fileContent(file) || '';
|
|
354
|
+
for (const match of content.matchAll(/type\s+\w+\s+interface\s*\{([\s\S]*?)\}/g)) {
|
|
355
|
+
blocks.push(match[1]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
ctx.__nerviqGoInterfaces = blocks;
|
|
359
|
+
return ctx.__nerviqGoInterfaces;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function countGoInterfaceMethods(block) {
|
|
363
|
+
return block
|
|
364
|
+
.split(/\r?\n/)
|
|
365
|
+
.map(line => line.trim())
|
|
366
|
+
.filter(line => line && !line.startsWith('//') && !line.startsWith('/*'))
|
|
367
|
+
.length;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const { containsEmbeddedSecret } = require('../secret-patterns');
|
|
371
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
372
|
+
const { buildSupplementalChecks } = require('../supplemental-checks');
|
|
373
|
+
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
374
|
+
|
|
375
|
+
const CLAUDE_SUPPLEMENTAL_SOURCE_URLS = {
|
|
376
|
+
'testing-strategy': 'https://code.claude.com/docs/en/common-workflows',
|
|
377
|
+
'code-quality': 'https://code.claude.com/docs/en/best-practices',
|
|
378
|
+
'api-design': 'https://code.claude.com/docs/en/best-practices',
|
|
379
|
+
database: 'https://code.claude.com/docs/en/common-workflows',
|
|
380
|
+
authentication: 'https://code.claude.com/docs/en/permissions',
|
|
381
|
+
monitoring: 'https://code.claude.com/docs/en/common-workflows',
|
|
382
|
+
'dependency-management': 'https://code.claude.com/docs/en/best-practices',
|
|
383
|
+
'cost-optimization': 'https://code.claude.com/docs/en/memory',
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
fs,
|
|
388
|
+
path,
|
|
389
|
+
collectClaudeDenyRules,
|
|
390
|
+
hasPromptInjectionDefenseGuidance,
|
|
391
|
+
hasMcpPromptInjectionDefenseGuidance,
|
|
392
|
+
hasInjectionDefenseHookConfigured,
|
|
393
|
+
getClaudeInstructionBundle,
|
|
394
|
+
getRepoInstructionBundle,
|
|
395
|
+
hasDocumentedVerificationGuidance,
|
|
396
|
+
hasDocumentedTestCommand,
|
|
397
|
+
hasDocumentedLintCommand,
|
|
398
|
+
hasDocumentedBuildCommand,
|
|
399
|
+
hasFrontendSignals,
|
|
400
|
+
getClaudeHookContents,
|
|
401
|
+
matchesPattern,
|
|
402
|
+
getProjectEntries,
|
|
403
|
+
getProjectFiles,
|
|
404
|
+
findProjectFiles,
|
|
405
|
+
hasProjectFile,
|
|
406
|
+
EXCLUDED_STACK_DIRS,
|
|
407
|
+
hasCoreProjectFile,
|
|
408
|
+
hasCoreRootFile,
|
|
409
|
+
readProjectFiles,
|
|
410
|
+
isPythonProject,
|
|
411
|
+
isGoProject,
|
|
412
|
+
isRustProject,
|
|
413
|
+
isJavaProject,
|
|
414
|
+
isFlutterProject,
|
|
415
|
+
isSwiftProject,
|
|
416
|
+
isKotlinProject,
|
|
417
|
+
isRubyProject,
|
|
418
|
+
isPhpProject,
|
|
419
|
+
isDotnetProject,
|
|
420
|
+
STACK_CATEGORY_DETECTORS,
|
|
421
|
+
getPythonFiles,
|
|
422
|
+
getMainPythonFiles,
|
|
423
|
+
getPythonProjectText,
|
|
424
|
+
getGoFiles,
|
|
425
|
+
getRustFiles,
|
|
426
|
+
getMainRustFiles,
|
|
427
|
+
getJavaFiles,
|
|
428
|
+
getMainJavaFiles,
|
|
429
|
+
getMainGoFiles,
|
|
430
|
+
getWorkflowContent,
|
|
431
|
+
getPreCommitContent,
|
|
432
|
+
getGoProjectText,
|
|
433
|
+
getRustProjectText,
|
|
434
|
+
getJavaBuildText,
|
|
435
|
+
getJavaProjectText,
|
|
436
|
+
getGoInterfaceBlocks,
|
|
437
|
+
countGoInterfaceMethods,
|
|
438
|
+
containsEmbeddedSecret,
|
|
439
|
+
attachSourceUrls,
|
|
440
|
+
buildSupplementalChecks,
|
|
441
|
+
resolveProjectStateReadPath,
|
|
442
|
+
CLAUDE_SUPPLEMENTAL_SOURCE_URLS,
|
|
443
|
+
};
|