@nerviq/cli 1.9.0 → 1.11.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 +106 -60
- package/bin/cli.js +382 -208
- package/package.json +1 -1
- package/src/activity.js +245 -63
- package/src/aider/freshness.js +149 -146
- package/src/analyze.js +3 -1
- package/src/anti-patterns.js +17 -13
- package/src/audit.js +106 -79
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +67 -51
- package/src/dashboard.js +36 -14
- package/src/governance.js +13 -7
- package/src/index.js +2 -1
- package/src/instruction-surfaces.js +185 -0
- package/src/integrations.js +102 -55
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/permission-rules.js +218 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +2 -2
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +102 -103
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/workspace.js +155 -13
package/src/governance.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const { DOMAIN_PACKS } = require('./domain-packs');
|
|
2
|
-
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
|
-
const { getCodexGovernanceSummary } = require('./codex/governance');
|
|
1
|
+
const { DOMAIN_PACKS } = require('./domain-packs');
|
|
2
|
+
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
|
+
const { getCodexGovernanceSummary } = require('./codex/governance');
|
|
4
|
+
const { formatTerminologyLines } = require('./terminology');
|
|
4
5
|
|
|
5
6
|
const PERMISSION_PROFILES = [
|
|
6
7
|
{
|
|
@@ -406,10 +407,15 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
406
407
|
console.log('');
|
|
407
408
|
console.log(` nerviq ${summary.platformLabel.toLowerCase()} governance`);
|
|
408
409
|
console.log(' ═══════════════════════════════════════');
|
|
409
|
-
console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
|
|
410
|
-
console.log('');
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
|
|
411
|
+
console.log('');
|
|
412
|
+
|
|
413
|
+
for (const line of formatTerminologyLines(['governance', 'hooks', 'denyRules', 'mcp'])) {
|
|
414
|
+
console.log(line);
|
|
415
|
+
}
|
|
416
|
+
console.log('');
|
|
417
|
+
|
|
418
|
+
console.log(' Permission Profiles');
|
|
413
419
|
for (const profile of summary.permissionProfiles) {
|
|
414
420
|
console.log(` - ${profile.label} [${profile.risk}]`);
|
|
415
421
|
console.log(` ${profile.useWhen}`);
|
package/src/index.js
CHANGED
|
@@ -88,7 +88,7 @@ const { getOpenCodeGovernanceSummary } = require('./opencode/governance');
|
|
|
88
88
|
const { runOpenCodeDeepReview } = require('./opencode/deep-review');
|
|
89
89
|
const { opencodeInteractive } = require('./opencode/interactive');
|
|
90
90
|
const { detectPlatforms, getCatalog, synergyReport } = require('./public-api');
|
|
91
|
-
const { createServer, startServer } = require('./server');
|
|
91
|
+
const { buildServeOpenApiSpec, createServer, startServer } = require('./server');
|
|
92
92
|
|
|
93
93
|
module.exports = {
|
|
94
94
|
audit,
|
|
@@ -101,6 +101,7 @@ module.exports = {
|
|
|
101
101
|
detectPlatforms,
|
|
102
102
|
getCatalog,
|
|
103
103
|
synergyReport,
|
|
104
|
+
buildServeOpenApiSpec,
|
|
104
105
|
createServer,
|
|
105
106
|
startServer,
|
|
106
107
|
DOMAIN_PACKS,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const TEST_COMMAND_PATTERNS = [
|
|
4
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+test\b/i,
|
|
5
|
+
/\b(?:python\s+-m\s+)?pytest\b/i,
|
|
6
|
+
/\bpython\s+manage\.py\s+test\b/i,
|
|
7
|
+
/\bdjango-admin\s+test\b/i,
|
|
8
|
+
/\bpython\s+-m\s+unittest\b/i,
|
|
9
|
+
/\bgo\s+test(?:\s|$)/i,
|
|
10
|
+
/\bcargo\s+test\b/i,
|
|
11
|
+
/\bmake\s+test\b/i,
|
|
12
|
+
/\bmix\s+test\b/i,
|
|
13
|
+
/\bbundle\s+exec\s+rspec\b/i,
|
|
14
|
+
/\brspec\b/i,
|
|
15
|
+
/\bphpunit\b/i,
|
|
16
|
+
/\bdotnet\s+test(?:\s|$)/i,
|
|
17
|
+
/\bflutter\s+test\b/i,
|
|
18
|
+
/\bswift\s+test\b/i,
|
|
19
|
+
/\bxcodebuild\b[^\n\r`]{0,200}\btest\b/i,
|
|
20
|
+
/\bgradlew?\s+test\b/i,
|
|
21
|
+
/\bmvn(?:w)?\s+test\b/i,
|
|
22
|
+
/\bplaywright\s+test\b/i,
|
|
23
|
+
/\bcypress\s+run\b/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const LINT_COMMAND_PATTERNS = [
|
|
27
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+lint\b/i,
|
|
28
|
+
/\beslint\b/i,
|
|
29
|
+
/\bprettier\b/i,
|
|
30
|
+
/\bruff(?:\s+(?:check|format))?\b/i,
|
|
31
|
+
/\bblack\b/i,
|
|
32
|
+
/\bflake8\b/i,
|
|
33
|
+
/\bpylint\b/i,
|
|
34
|
+
/\bmypy\b/i,
|
|
35
|
+
/\bpyright\b/i,
|
|
36
|
+
/\bgo\s+vet\b/i,
|
|
37
|
+
/\bgofmt\b/i,
|
|
38
|
+
/\bgofumpt\b/i,
|
|
39
|
+
/\bstaticcheck\b/i,
|
|
40
|
+
/\bgolangci-lint\b/i,
|
|
41
|
+
/\bcargo\s+clippy\b/i,
|
|
42
|
+
/\bflutter\s+analyze\b/i,
|
|
43
|
+
/\bdart\s+analyze\b/i,
|
|
44
|
+
/\bswiftlint\b/i,
|
|
45
|
+
/\bswift(?:-|\s+)format\b/i,
|
|
46
|
+
/\bdotnet\s+format(?:\s|$)/i,
|
|
47
|
+
/\bgradlew?\s+lint\b/i,
|
|
48
|
+
/\bmvn(?:w)?\s+(?:checkstyle:check|spotbugs:check|verify)\b/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const BUILD_COMMAND_PATTERNS = [
|
|
52
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+build\b/i,
|
|
53
|
+
/\btsc(?:\s|$)/i,
|
|
54
|
+
/\bgo\s+build(?:\s|$)/i,
|
|
55
|
+
/\bcargo\s+(?:build|check)\b/i,
|
|
56
|
+
/\bmake\s+build\b/i,
|
|
57
|
+
/\bdotnet\s+(?:build|publish)(?:\s|$)/i,
|
|
58
|
+
/\bmsbuild\b/i,
|
|
59
|
+
/\bflutter\s+build(?:\s|$)/i,
|
|
60
|
+
/\bswift\s+build\b/i,
|
|
61
|
+
/\bxcodebuild\b/i,
|
|
62
|
+
/\bgradlew?\s+(?:build|assemble)\b/i,
|
|
63
|
+
/\bmvn(?:w)?\s+(?:compile|package|verify|install)\b/i,
|
|
64
|
+
/\bpython\s+-m\s+build\b/i,
|
|
65
|
+
/\bpoetry\s+build\b/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function normalizePath(filePath) {
|
|
69
|
+
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addSurface(ctx, surfaces, seen, filePath) {
|
|
73
|
+
const normalized = normalizePath(filePath);
|
|
74
|
+
if (!normalized || seen.has(normalized)) return;
|
|
75
|
+
const content = typeof ctx.fileContent === 'function' ? (ctx.fileContent(normalized) || '') : '';
|
|
76
|
+
if (!content.trim()) return;
|
|
77
|
+
seen.add(normalized);
|
|
78
|
+
surfaces.push({ path: normalized, content });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addDirSurfaces(ctx, surfaces, seen, dirPath, filter) {
|
|
82
|
+
if (typeof ctx.dirFiles !== 'function') return;
|
|
83
|
+
for (const entry of ctx.dirFiles(dirPath) || []) {
|
|
84
|
+
if (filter && !filter.test(entry)) continue;
|
|
85
|
+
addSurface(ctx, surfaces, seen, path.join(dirPath, entry));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildSurfaceList(ctx, scope) {
|
|
90
|
+
const surfaces = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const includeReadme = scope === 'repo';
|
|
93
|
+
|
|
94
|
+
addSurface(ctx, surfaces, seen, 'CLAUDE.md');
|
|
95
|
+
addSurface(ctx, surfaces, seen, '.claude/CLAUDE.md');
|
|
96
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/rules', /\.md$/i);
|
|
97
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/commands', /\.md$/i);
|
|
98
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/agents', /\.md$/i);
|
|
99
|
+
|
|
100
|
+
if (scope === 'repo') {
|
|
101
|
+
addSurface(ctx, surfaces, seen, 'AGENTS.md');
|
|
102
|
+
addSurface(ctx, surfaces, seen, 'AGENTS.override.md');
|
|
103
|
+
addSurface(ctx, surfaces, seen, '.cursorrules');
|
|
104
|
+
addSurface(ctx, surfaces, seen, '.windsurfrules');
|
|
105
|
+
addSurface(ctx, surfaces, seen, 'GEMINI.md');
|
|
106
|
+
addSurface(ctx, surfaces, seen, '.gemini/GEMINI.md');
|
|
107
|
+
addSurface(ctx, surfaces, seen, '.github/copilot-instructions.md');
|
|
108
|
+
addDirSurfaces(ctx, surfaces, seen, '.cursor/rules', /\.(md|mdc)$/i);
|
|
109
|
+
addDirSurfaces(ctx, surfaces, seen, '.cursor/commands', /\.md$/i);
|
|
110
|
+
addDirSurfaces(ctx, surfaces, seen, '.windsurf/rules', /\.md$/i);
|
|
111
|
+
addDirSurfaces(ctx, surfaces, seen, '.windsurf/workflows', /\.md$/i);
|
|
112
|
+
addDirSurfaces(ctx, surfaces, seen, '.github/instructions', /\.instructions\.md$/i);
|
|
113
|
+
addDirSurfaces(ctx, surfaces, seen, '.github/prompts', /\.prompt\.md$/i);
|
|
114
|
+
addDirSurfaces(ctx, surfaces, seen, '.opencode/commands', /\.(md|markdown|ya?ml)$/i);
|
|
115
|
+
addDirSurfaces(ctx, surfaces, seen, '.gemini/agents', /\.md$/i);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (includeReadme) {
|
|
119
|
+
addSurface(ctx, surfaces, seen, 'README.md');
|
|
120
|
+
addSurface(ctx, surfaces, seen, 'CONTRIBUTING.md');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return surfaces;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getSurfaceBundle(ctx, scope) {
|
|
127
|
+
const cacheKey = scope === 'repo' ? '__nerviqRepoInstructionBundle' : '__nerviqClaudeInstructionBundle';
|
|
128
|
+
if (ctx && ctx[cacheKey] !== undefined) return ctx[cacheKey];
|
|
129
|
+
const bundle = buildSurfaceList(ctx, scope)
|
|
130
|
+
.map((surface) => surface.content)
|
|
131
|
+
.join('\n\n');
|
|
132
|
+
if (ctx) ctx[cacheKey] = bundle;
|
|
133
|
+
return bundle;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function matchesAny(text, patterns) {
|
|
137
|
+
const normalized = String(text || '');
|
|
138
|
+
return patterns.some((pattern) => {
|
|
139
|
+
pattern.lastIndex = 0;
|
|
140
|
+
return pattern.test(normalized);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasDocumentedTestCommand(text) {
|
|
145
|
+
return matchesAny(text, TEST_COMMAND_PATTERNS);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasDocumentedLintCommand(text) {
|
|
149
|
+
return matchesAny(text, LINT_COMMAND_PATTERNS);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasDocumentedBuildCommand(text) {
|
|
153
|
+
return matchesAny(text, BUILD_COMMAND_PATTERNS);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hasDocumentedVerificationGuidance(text) {
|
|
157
|
+
const normalized = String(text || '');
|
|
158
|
+
if (!normalized.trim()) return false;
|
|
159
|
+
return hasDocumentedTestCommand(normalized) ||
|
|
160
|
+
hasDocumentedLintCommand(normalized) ||
|
|
161
|
+
hasDocumentedBuildCommand(normalized) ||
|
|
162
|
+
(/\b(?:verification|verify|self-check|quality gate)\b/i.test(normalized) &&
|
|
163
|
+
matchesAny(normalized, [
|
|
164
|
+
...TEST_COMMAND_PATTERNS,
|
|
165
|
+
...LINT_COMMAND_PATTERNS,
|
|
166
|
+
...BUILD_COMMAND_PATTERNS,
|
|
167
|
+
]));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getClaudeInstructionBundle(ctx) {
|
|
171
|
+
return getSurfaceBundle(ctx, 'claude');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getRepoInstructionBundle(ctx) {
|
|
175
|
+
return getSurfaceBundle(ctx, 'repo');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
getClaudeInstructionBundle,
|
|
180
|
+
getRepoInstructionBundle,
|
|
181
|
+
hasDocumentedVerificationGuidance,
|
|
182
|
+
hasDocumentedTestCommand,
|
|
183
|
+
hasDocumentedLintCommand,
|
|
184
|
+
hasDocumentedBuildCommand,
|
|
185
|
+
};
|
package/src/integrations.js
CHANGED
|
@@ -10,66 +10,113 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const https = require('https');
|
|
14
|
-
const http = require('http');
|
|
15
|
-
const { URL } = require('url');
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const { URL } = require('url');
|
|
16
|
+
|
|
17
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
18
|
+
|
|
19
|
+
function wait(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sendWebhookOnce(parsed, body, opts = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
26
|
+
const customHeaders = opts.headers || {};
|
|
27
|
+
const headers = {
|
|
28
|
+
'User-Agent': `nerviq/${require('../package.json').version}`,
|
|
29
|
+
...customHeaders,
|
|
30
|
+
'Content-Length': Buffer.byteLength(body),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const hasContentTypeHeader = Object.keys(headers).some((name) => name.toLowerCase() === 'content-type');
|
|
34
|
+
if (!hasContentTypeHeader) {
|
|
35
|
+
headers['Content-Type'] = 'application/json';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const options = {
|
|
39
|
+
hostname: parsed.hostname,
|
|
40
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
41
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
47
|
+
|
|
48
|
+
const req = transport.request(options, (res) => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
res.on('data', (c) => chunks.push(c));
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
const respBody = Buffer.concat(chunks).toString('utf8');
|
|
53
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: respBody });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
req.setTimeout(timeoutMs, () => {
|
|
58
|
+
req.destroy(new Error(`Webhook request timed out after ${timeoutMs}ms`));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
req.on('error', reject);
|
|
62
|
+
req.write(body);
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Webhook delivery ────────────────────────────────────────────────────────
|
|
18
68
|
|
|
19
69
|
/**
|
|
20
70
|
* POST JSON payload to a webhook URL.
|
|
21
71
|
* @param {string} url - Destination URL (http or https)
|
|
22
72
|
* @param {object} payload - JSON-serialisable object
|
|
23
|
-
* @param {object} [opts]
|
|
24
|
-
* @param {number} [opts.timeoutMs=10000]
|
|
25
|
-
* @param {object} [opts.headers]
|
|
26
|
-
* @
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
req.end();
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
+
* @param {object} [opts]
|
|
74
|
+
* @param {number} [opts.timeoutMs=10000]
|
|
75
|
+
* @param {object} [opts.headers]
|
|
76
|
+
* @param {number} [opts.retries=2]
|
|
77
|
+
* @param {number} [opts.retryDelayMs=400]
|
|
78
|
+
* @returns {Promise<{ ok: boolean, status: number, body: string, attempts: number }>}
|
|
79
|
+
*/
|
|
80
|
+
async function sendWebhook(url, payload, opts = {}) {
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = new URL(url);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error(`Invalid webhook URL: ${url}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
89
|
+
throw new Error(`Unsupported webhook protocol: ${parsed.protocol}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const body = JSON.stringify(payload);
|
|
93
|
+
const retries = Number.isInteger(opts.retries) && opts.retries >= 0 ? opts.retries : 2;
|
|
94
|
+
const retryDelayMs = Number.isFinite(opts.retryDelayMs) && opts.retryDelayMs >= 0 ? opts.retryDelayMs : 400;
|
|
95
|
+
const maxAttempts = retries + 1;
|
|
96
|
+
|
|
97
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
98
|
+
try {
|
|
99
|
+
const response = await sendWebhookOnce(parsed, body, opts);
|
|
100
|
+
const enriched = { ...response, attempts: attempt };
|
|
101
|
+
const shouldRetry = RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxAttempts;
|
|
102
|
+
if (!shouldRetry) {
|
|
103
|
+
return enriched;
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
error.attempts = attempt;
|
|
107
|
+
if (attempt >= maxAttempts) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const delayMs = retryDelayMs * attempt;
|
|
113
|
+
if (delayMs > 0) {
|
|
114
|
+
await wait(delayMs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { ok: false, status: 0, body: '', attempts: maxAttempts };
|
|
119
|
+
}
|
|
73
120
|
|
|
74
121
|
// ─── Slack formatting ─────────────────────────────────────────────────────────
|
|
75
122
|
|
package/src/locales/en.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"audit.platform": "Platform: {platform} ({version})",
|
|
8
8
|
"audit.domainPacks": "Domain packs: {packs}",
|
|
9
9
|
"audit.scope": "Scope: {message}",
|
|
10
|
-
"audit.score": "
|
|
10
|
+
"audit.score": "Live audit score: {score} ({passed}/{total} checks passing)",
|
|
11
11
|
"audit.found": "Found: {files}",
|
|
12
12
|
"audit.excellent": "Excellent setup — production-ready governance",
|
|
13
13
|
"audit.strong": "Strong setup — {count} critical items to address",
|
package/src/locales/es.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"audit.platform": "Plataforma: {platform} ({version})",
|
|
8
8
|
"audit.domainPacks": "Paquetes de dominio: {packs}",
|
|
9
9
|
"audit.scope": "Alcance: {message}",
|
|
10
|
-
"audit.score": "Puntuación: {score}
|
|
10
|
+
"audit.score": "Puntuación de auditoría en vivo: {score} ({passed}/{total} verificaciones aprobadas)",
|
|
11
11
|
"audit.found": "Encontrado: {files}",
|
|
12
12
|
"audit.excellent": "Configuración excelente — gobernanza lista para producción",
|
|
13
13
|
"audit.strong": "Configuración sólida — {count} elementos críticos por resolver",
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const PATH_ACTIONS = new Set(['read', 'write', 'edit', 'multiedit']);
|
|
5
|
+
const SECRET_PATH_RE = /(^|\/)(\.env(?:[^/]*)?|secrets?)(\/|$)/i;
|
|
6
|
+
|
|
7
|
+
function normalizeSlash(value) {
|
|
8
|
+
return String(value || '').replace(/\\/g, '/');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripWrappingQuotes(value) {
|
|
12
|
+
const trimmed = String(value || '').trim();
|
|
13
|
+
if (!trimmed) return '';
|
|
14
|
+
const first = trimmed[0];
|
|
15
|
+
const last = trimmed[trimmed.length - 1];
|
|
16
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
17
|
+
return trimmed.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getProjectRoot(rootDir) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.realpathSync.native(rootDir);
|
|
25
|
+
} catch {
|
|
26
|
+
return path.resolve(rootDir);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function splitPatternSegments(rawPattern, isAbsolute) {
|
|
31
|
+
const normalized = normalizeSlash(rawPattern);
|
|
32
|
+
|
|
33
|
+
if (/^[A-Za-z]:\//.test(normalized)) {
|
|
34
|
+
return normalized.slice(3).split('/').filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isAbsolute && normalized.startsWith('/')) {
|
|
38
|
+
return normalized.slice(1).split('/').filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return normalized.split('/').filter((segment) => segment && segment !== '.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasGlob(segment) {
|
|
45
|
+
return /[*?[\]{}]/.test(segment);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildAbsolutePattern(rootDir, rawPattern) {
|
|
49
|
+
const normalized = stripWrappingQuotes(normalizeSlash(rawPattern).replace(/^file:\/\//i, ''));
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
return {
|
|
52
|
+
absolutePattern: null,
|
|
53
|
+
normalizedInput: '',
|
|
54
|
+
isAbsolute: false,
|
|
55
|
+
traversalSegments: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isAbsolute = /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('/');
|
|
60
|
+
const traversalSegments = normalized.split('/').some((segment) => segment === '..');
|
|
61
|
+
const segments = splitPatternSegments(normalized, isAbsolute);
|
|
62
|
+
let current = isAbsolute ? path.parse(path.resolve(normalized)).root : getProjectRoot(rootDir);
|
|
63
|
+
|
|
64
|
+
for (const segment of segments) {
|
|
65
|
+
const candidate = path.join(current, segment);
|
|
66
|
+
if (hasGlob(segment)) {
|
|
67
|
+
current = candidate;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
current = fs.realpathSync.native(candidate);
|
|
73
|
+
} catch {
|
|
74
|
+
current = candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
absolutePattern: current,
|
|
80
|
+
normalizedInput: normalized,
|
|
81
|
+
isAbsolute,
|
|
82
|
+
traversalSegments,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizePathPayload(rawPayload, rootDir) {
|
|
87
|
+
const {
|
|
88
|
+
absolutePattern,
|
|
89
|
+
normalizedInput,
|
|
90
|
+
isAbsolute,
|
|
91
|
+
traversalSegments,
|
|
92
|
+
} = buildAbsolutePattern(rootDir, rawPayload);
|
|
93
|
+
|
|
94
|
+
if (!absolutePattern) {
|
|
95
|
+
return {
|
|
96
|
+
normalizedPath: '',
|
|
97
|
+
repoRelativePath: '',
|
|
98
|
+
outsideRepo: false,
|
|
99
|
+
invalid: true,
|
|
100
|
+
isAbsolute,
|
|
101
|
+
traversalSegments,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const projectRoot = getProjectRoot(rootDir);
|
|
106
|
+
const relativePath = normalizeSlash(path.relative(projectRoot, absolutePattern));
|
|
107
|
+
const outsideRepo = relativePath === '..' || relativePath.startsWith('../') || /^[A-Za-z]:\//.test(relativePath);
|
|
108
|
+
const repoRelativePath = outsideRepo ? null : relativePath || '.';
|
|
109
|
+
const normalizedPath = outsideRepo
|
|
110
|
+
? normalizeSlash(absolutePattern)
|
|
111
|
+
: `./${repoRelativePath}`;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
normalizedPath,
|
|
115
|
+
repoRelativePath,
|
|
116
|
+
outsideRepo,
|
|
117
|
+
invalid: traversalSegments && outsideRepo && !isAbsolute,
|
|
118
|
+
isAbsolute,
|
|
119
|
+
traversalSegments,
|
|
120
|
+
normalizedInput,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeCommandPayload(rawPayload) {
|
|
125
|
+
return stripWrappingQuotes(rawPayload).replace(/\s+/g, ' ').trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizePermissionRule(rule, rootDir) {
|
|
129
|
+
if (typeof rule !== 'string' || !rule.trim()) return null;
|
|
130
|
+
const trimmed = rule.trim();
|
|
131
|
+
const match = trimmed.match(/^([A-Za-z]+)\((.*)\)$/);
|
|
132
|
+
if (!match) {
|
|
133
|
+
return {
|
|
134
|
+
raw: trimmed,
|
|
135
|
+
action: null,
|
|
136
|
+
payload: trimmed,
|
|
137
|
+
normalized: trimmed,
|
|
138
|
+
dedupeKey: trimmed.toLowerCase(),
|
|
139
|
+
kind: 'raw',
|
|
140
|
+
invalid: false,
|
|
141
|
+
outsideRepo: false,
|
|
142
|
+
protectsSecrets: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const action = match[1];
|
|
147
|
+
const payload = match[2].trim();
|
|
148
|
+
const actionKey = action.toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (PATH_ACTIONS.has(actionKey)) {
|
|
151
|
+
const details = normalizePathPayload(payload, rootDir);
|
|
152
|
+
const dedupeKey = `${actionKey}:${details.normalizedPath.toLowerCase()}`;
|
|
153
|
+
return {
|
|
154
|
+
raw: trimmed,
|
|
155
|
+
action,
|
|
156
|
+
payload,
|
|
157
|
+
normalized: `${action}(${details.normalizedPath})`,
|
|
158
|
+
normalizedPath: details.normalizedPath,
|
|
159
|
+
repoRelativePath: details.repoRelativePath,
|
|
160
|
+
dedupeKey,
|
|
161
|
+
kind: 'path',
|
|
162
|
+
invalid: details.invalid,
|
|
163
|
+
outsideRepo: details.outsideRepo,
|
|
164
|
+
traversalSegments: details.traversalSegments,
|
|
165
|
+
isAbsolute: details.isAbsolute,
|
|
166
|
+
protectsSecrets: !details.outsideRepo && SECRET_PATH_RE.test(details.repoRelativePath || ''),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedPayload = normalizeCommandPayload(payload);
|
|
171
|
+
return {
|
|
172
|
+
raw: trimmed,
|
|
173
|
+
action,
|
|
174
|
+
payload,
|
|
175
|
+
normalized: `${action}(${normalizedPayload})`,
|
|
176
|
+
dedupeKey: `${actionKey}:${normalizedPayload.toLowerCase()}`,
|
|
177
|
+
kind: 'command',
|
|
178
|
+
invalid: false,
|
|
179
|
+
outsideRepo: false,
|
|
180
|
+
protectsSecrets: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizePermissionRules(rules, rootDir) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const normalized = [];
|
|
187
|
+
|
|
188
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
189
|
+
const entry = normalizePermissionRule(rule, rootDir);
|
|
190
|
+
if (!entry || entry.invalid) continue;
|
|
191
|
+
if (seen.has(entry.dedupeKey)) continue;
|
|
192
|
+
seen.add(entry.dedupeKey);
|
|
193
|
+
normalized.push(entry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function collectClaudeDenyRules(ctx) {
|
|
200
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
201
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
202
|
+
const denyRules = []
|
|
203
|
+
.concat(shared?.permissions?.deny || [])
|
|
204
|
+
.concat(local?.permissions?.deny || []);
|
|
205
|
+
|
|
206
|
+
return normalizePermissionRules(denyRules, ctx.dir);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasSecretDenyRule(rules) {
|
|
210
|
+
return (Array.isArray(rules) ? rules : []).some((rule) => rule && rule.protectsSecrets);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
collectClaudeDenyRules,
|
|
215
|
+
hasSecretDenyRule,
|
|
216
|
+
normalizePermissionRule,
|
|
217
|
+
normalizePermissionRules,
|
|
218
|
+
};
|
package/src/secret-patterns.js
CHANGED
|
@@ -5,6 +5,15 @@ const EMBEDDED_SECRET_PATTERNS = [
|
|
|
5
5
|
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
6
6
|
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
7
7
|
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
8
|
+
/\bAZURE(?:_[A-Z0-9]+){0,4}_(?:API_)?KEY\s*[:=]\s*['"]?[A-Za-z0-9+/=_-]{20,}['"]?/g,
|
|
9
|
+
/\bDefaultEndpointsProtocol=https;AccountName=[^;\s]+;AccountKey=[A-Za-z0-9+/=]{20,};EndpointSuffix=core\.windows\.net\b/gi,
|
|
10
|
+
/\bEndpoint=sb:\/\/[^\s;]+;SharedAccessKeyName=[^;\s]+;SharedAccessKey=[A-Za-z0-9+/=]{20,}\b/gi,
|
|
11
|
+
/\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|rediss|amqp):\/\/[^:\s/]+:[^@\s]{4,}@[^/\s]+(?:\/[^\s'"]*)?/gi,
|
|
12
|
+
/\b(?:Server|Host|Data Source)\s*=\s*[^;\n]+;[^\n]*(?:Password|Pwd)\s*=\s*[^;\n]{4,}/gi,
|
|
13
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
14
|
+
/-----BEGIN (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----[\s\S]{40,}?-----END (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----/g,
|
|
15
|
+
/-----BEGIN PRIVATE KEY-----[\s\S]{40,}?-----END PRIVATE KEY-----/g,
|
|
16
|
+
/"type"\s*:\s*"service_account"[\s\S]{0,1200}?"client_email"\s*:\s*"[^\"]+@[^"]*gserviceaccount\.com"[\s\S]{0,1200}?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]{20,}?-----END PRIVATE KEY-----\\n?"/g,
|
|
8
17
|
];
|
|
9
18
|
|
|
10
19
|
function containsEmbeddedSecret(text = '') {
|