cc-env-checker 0.1.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 +413 -0
- package/package.json +35 -0
- package/src/checks/artifacts.js +318 -0
- package/src/checks/config.js +174 -0
- package/src/checks/fingerprint.js +192 -0
- package/src/checks/helpers.js +326 -0
- package/src/checks/install.js +72 -0
- package/src/checks/network.js +443 -0
- package/src/checks/runtime.js +106 -0
- package/src/checks/static-install.js +307 -0
- package/src/cli-app.js +108 -0
- package/src/cli.js +50 -0
- package/src/doctor.js +67 -0
- package/src/i18n.js +405 -0
- package/src/modules.js +25 -0
- package/src/render.js +189 -0
- package/src/report.js +165 -0
- package/src/types.js +18 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createCheckResult,
|
|
6
|
+
extractMatches,
|
|
7
|
+
resolveExecutable,
|
|
8
|
+
} from './helpers.js';
|
|
9
|
+
|
|
10
|
+
const ENDPOINT_GROUPS = [
|
|
11
|
+
{
|
|
12
|
+
id: 'anthropic-api',
|
|
13
|
+
patterns: [/https:\/\/api\.anthropic\.com\/[^\s"'`]+/g],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'anthropic-mcp-proxy',
|
|
17
|
+
patterns: [/https:\/\/mcp-proxy\.anthropic\.com[^\s"'`]*/g],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'datadog-logs',
|
|
21
|
+
patterns: [/https:\/\/http-intake\.logs\.[^\s"'`]+/g],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const HEADER_GROUPS = [
|
|
26
|
+
{
|
|
27
|
+
id: 'anthropic',
|
|
28
|
+
patterns: [/\bx-anthropic-[a-z0-9-]+\b/gi],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'claude-code',
|
|
32
|
+
patterns: [/\bx-claude-code-[a-z0-9-]+\b/gi],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'generic',
|
|
36
|
+
patterns: [/\bx-client-request-id\b/gi, /\buser-agent\b/gi],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const EVENT_GROUPS = [
|
|
41
|
+
{
|
|
42
|
+
id: 'tengu',
|
|
43
|
+
patterns: [/\btengu_[a-z0-9_]+\b/g],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const DEFAULT_INSTALL_MAX_FILE_BYTES = 128 * 1024;
|
|
48
|
+
const DEFAULT_INSTALL_MAX_FILES = 24;
|
|
49
|
+
const READABLE_INSTALL_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.json']);
|
|
50
|
+
|
|
51
|
+
function summarizeGroupMatches(text, groups) {
|
|
52
|
+
return groups
|
|
53
|
+
.map((group) => ({
|
|
54
|
+
id: group.id,
|
|
55
|
+
count: extractMatches(text, group.patterns).length,
|
|
56
|
+
}))
|
|
57
|
+
.filter((group) => group.count > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatCategoryDetails(groups, prefix) {
|
|
61
|
+
if (!groups.length) {
|
|
62
|
+
return [`${prefix}=none-observed`];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return groups.map((group) => `${group.id}=${group.count}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sanitizeErrorLabel(error) {
|
|
69
|
+
if (error?.code) {
|
|
70
|
+
return `unreadable:${error.code}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return 'unreadable:unknown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createSummaryCheck({
|
|
77
|
+
id,
|
|
78
|
+
titleKey,
|
|
79
|
+
details,
|
|
80
|
+
summaryKey,
|
|
81
|
+
suggestionKey,
|
|
82
|
+
evidenceType = 'observed',
|
|
83
|
+
status = 'pass',
|
|
84
|
+
}) {
|
|
85
|
+
return createCheckResult({
|
|
86
|
+
id,
|
|
87
|
+
titleKey,
|
|
88
|
+
status,
|
|
89
|
+
riskLevel: 'low',
|
|
90
|
+
evidenceType,
|
|
91
|
+
source: 'static-install',
|
|
92
|
+
summaryKey,
|
|
93
|
+
details,
|
|
94
|
+
suggestionKey,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolvePackageRoot(executablePath) {
|
|
99
|
+
return path.resolve(
|
|
100
|
+
path.dirname(executablePath),
|
|
101
|
+
'..',
|
|
102
|
+
'lib',
|
|
103
|
+
'node_modules',
|
|
104
|
+
'claude-code',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function resolvePackageRoots(executablePath) {
|
|
109
|
+
const roots = [];
|
|
110
|
+
const pushUnique = (candidate) => {
|
|
111
|
+
if (candidate && !roots.includes(candidate)) {
|
|
112
|
+
roots.push(candidate);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
pushUnique(resolvePackageRoot(executablePath));
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const realExecutablePath = await fs.realpath(executablePath);
|
|
120
|
+
const scopedPackageRoot = path.dirname(realExecutablePath);
|
|
121
|
+
pushUnique(scopedPackageRoot);
|
|
122
|
+
|
|
123
|
+
if (path.basename(scopedPackageRoot) === 'bin') {
|
|
124
|
+
pushUnique(path.resolve(scopedPackageRoot, '..'));
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Fall back to the legacy derived package root when realpath is unavailable.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return roots;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function collectInstallTextFiles(rootPath, { maxFiles = DEFAULT_INSTALL_MAX_FILES } = {}) {
|
|
134
|
+
const selected = [];
|
|
135
|
+
const queue = [rootPath];
|
|
136
|
+
|
|
137
|
+
while (queue.length > 0 && selected.length < maxFiles) {
|
|
138
|
+
const currentPath = queue.shift();
|
|
139
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
140
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
141
|
+
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
144
|
+
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
queue.push(entryPath);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!entry.isFile()) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!READABLE_INSTALL_EXTENSIONS.has(path.extname(entry.name))) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const stat = await fs.stat(entryPath);
|
|
159
|
+
if (stat.size > DEFAULT_INSTALL_MAX_FILE_BYTES) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
selected.push(entryPath);
|
|
164
|
+
if (selected.length >= maxFiles) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return selected;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function defaultReadInstallText(executablePath) {
|
|
174
|
+
const packageRoots = await resolvePackageRoots(executablePath);
|
|
175
|
+
let lastError = null;
|
|
176
|
+
|
|
177
|
+
for (const packageRoot of packageRoots) {
|
|
178
|
+
try {
|
|
179
|
+
const selectedFiles = await collectInstallTextFiles(packageRoot);
|
|
180
|
+
const chunks = await Promise.all(
|
|
181
|
+
selectedFiles.map((filePath) => fs.readFile(filePath, 'utf8')),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
text: chunks.join('\n'),
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
lastError = error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
code: lastError?.code,
|
|
196
|
+
message: lastError?.message,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createFailureChecks({ status, evidenceType, summaryKey, suggestionKey, details }) {
|
|
201
|
+
return [
|
|
202
|
+
createSummaryCheck({
|
|
203
|
+
id: 'static-install.endpoint-summary',
|
|
204
|
+
titleKey: 'check.staticInstall.endpoint.title',
|
|
205
|
+
status,
|
|
206
|
+
evidenceType,
|
|
207
|
+
summaryKey,
|
|
208
|
+
suggestionKey,
|
|
209
|
+
details,
|
|
210
|
+
}),
|
|
211
|
+
createSummaryCheck({
|
|
212
|
+
id: 'static-install.header-summary',
|
|
213
|
+
titleKey: 'check.staticInstall.header.title',
|
|
214
|
+
status,
|
|
215
|
+
evidenceType,
|
|
216
|
+
summaryKey,
|
|
217
|
+
suggestionKey,
|
|
218
|
+
details,
|
|
219
|
+
}),
|
|
220
|
+
createSummaryCheck({
|
|
221
|
+
id: 'static-install.event-summary',
|
|
222
|
+
titleKey: 'check.staticInstall.event.title',
|
|
223
|
+
status,
|
|
224
|
+
evidenceType,
|
|
225
|
+
summaryKey,
|
|
226
|
+
suggestionKey,
|
|
227
|
+
details,
|
|
228
|
+
}),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createStaticInstallModule({
|
|
233
|
+
resolveExecutable: resolve = resolveExecutable,
|
|
234
|
+
readInstallText = defaultReadInstallText,
|
|
235
|
+
} = {}) {
|
|
236
|
+
return {
|
|
237
|
+
id: 'static-install',
|
|
238
|
+
titleKey: 'module.staticInstall',
|
|
239
|
+
async run() {
|
|
240
|
+
const executable = await resolve('claude');
|
|
241
|
+
|
|
242
|
+
if (!executable.ok) {
|
|
243
|
+
return createFailureChecks({
|
|
244
|
+
status: 'skip',
|
|
245
|
+
evidenceType: 'unverifiable',
|
|
246
|
+
summaryKey: 'check.staticInstall.summary.skip',
|
|
247
|
+
suggestionKey: 'check.staticInstall.suggestion.skip',
|
|
248
|
+
details: ['install-text=unavailable'],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const textResult = await readInstallText(executable.path);
|
|
253
|
+
|
|
254
|
+
if (!textResult.ok) {
|
|
255
|
+
return createFailureChecks({
|
|
256
|
+
status: 'warn',
|
|
257
|
+
evidenceType: 'partial',
|
|
258
|
+
summaryKey: 'check.staticInstall.summary.warn',
|
|
259
|
+
suggestionKey: 'check.staticInstall.suggestion.warn',
|
|
260
|
+
details: [`install-text=${sanitizeErrorLabel(textResult)}`],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const endpointGroups = summarizeGroupMatches(textResult.text, ENDPOINT_GROUPS);
|
|
265
|
+
const headerGroups = summarizeGroupMatches(textResult.text, HEADER_GROUPS);
|
|
266
|
+
const eventGroups = summarizeGroupMatches(textResult.text, EVENT_GROUPS);
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
createSummaryCheck({
|
|
270
|
+
id: 'static-install.endpoint-summary',
|
|
271
|
+
titleKey: 'check.staticInstall.endpoint.title',
|
|
272
|
+
status: endpointGroups.length ? 'pass' : 'warn',
|
|
273
|
+
evidenceType: endpointGroups.length ? 'observed' : 'partial',
|
|
274
|
+
summaryKey: endpointGroups.length
|
|
275
|
+
? 'check.staticInstall.endpoint.summary.pass'
|
|
276
|
+
: 'check.staticInstall.endpoint.summary.warn',
|
|
277
|
+
details: formatCategoryDetails(endpointGroups, 'endpoint'),
|
|
278
|
+
suggestionKey: 'check.staticInstall.endpoint.suggestion.review',
|
|
279
|
+
}),
|
|
280
|
+
createSummaryCheck({
|
|
281
|
+
id: 'static-install.header-summary',
|
|
282
|
+
titleKey: 'check.staticInstall.header.title',
|
|
283
|
+
status: headerGroups.length ? 'pass' : 'warn',
|
|
284
|
+
evidenceType: headerGroups.length ? 'observed' : 'partial',
|
|
285
|
+
summaryKey: headerGroups.length
|
|
286
|
+
? 'check.staticInstall.header.summary.pass'
|
|
287
|
+
: 'check.staticInstall.header.summary.warn',
|
|
288
|
+
details: formatCategoryDetails(headerGroups, 'header'),
|
|
289
|
+
suggestionKey: 'check.staticInstall.header.suggestion.review',
|
|
290
|
+
}),
|
|
291
|
+
createSummaryCheck({
|
|
292
|
+
id: 'static-install.event-summary',
|
|
293
|
+
titleKey: 'check.staticInstall.event.title',
|
|
294
|
+
status: eventGroups.length ? 'pass' : 'warn',
|
|
295
|
+
evidenceType: eventGroups.length ? 'observed' : 'partial',
|
|
296
|
+
summaryKey: eventGroups.length
|
|
297
|
+
? 'check.staticInstall.event.summary.pass'
|
|
298
|
+
: 'check.staticInstall.event.summary.warn',
|
|
299
|
+
details: formatCategoryDetails(eventGroups, 'event'),
|
|
300
|
+
suggestionKey: 'check.staticInstall.event.suggestion.review',
|
|
301
|
+
}),
|
|
302
|
+
];
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const staticInstallModule = createStaticInstallModule();
|
package/src/cli-app.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createDoctorReport } from './doctor.js';
|
|
2
|
+
import { createTranslator, resolveLocale } from './i18n.js';
|
|
3
|
+
import { defaultModules, selectModules } from './modules.js';
|
|
4
|
+
import { renderHumanReport, renderJsonReport } from './render.js';
|
|
5
|
+
|
|
6
|
+
function toModuleTitleKey(moduleId) {
|
|
7
|
+
const normalized = String(moduleId).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
8
|
+
return `module.${normalized}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeArgs(argv) {
|
|
12
|
+
const flags = {
|
|
13
|
+
verbose: false,
|
|
14
|
+
json: false,
|
|
15
|
+
remote: true,
|
|
16
|
+
timeoutMs: 4000,
|
|
17
|
+
lang: undefined,
|
|
18
|
+
};
|
|
19
|
+
const positionals = [];
|
|
20
|
+
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const arg = argv[index];
|
|
23
|
+
if (arg === '--verbose') {
|
|
24
|
+
flags.verbose = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (arg === '--json') {
|
|
28
|
+
flags.json = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === '--no-remote') {
|
|
32
|
+
flags.remote = false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg === '--timeout') {
|
|
36
|
+
const next = argv[index + 1];
|
|
37
|
+
if (next) {
|
|
38
|
+
flags.timeoutMs = Number(next);
|
|
39
|
+
index += 1;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === '--lang') {
|
|
44
|
+
const next = argv[index + 1];
|
|
45
|
+
if (next) {
|
|
46
|
+
flags.lang = next;
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
positionals.push(arg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const command = positionals[0] ?? 'doctor';
|
|
55
|
+
|
|
56
|
+
return { command, flags };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildCli({
|
|
60
|
+
modules = defaultModules,
|
|
61
|
+
stdout = process.stdout,
|
|
62
|
+
stderr = process.stderr,
|
|
63
|
+
} = {}) {
|
|
64
|
+
return {
|
|
65
|
+
async run(argv = []) {
|
|
66
|
+
const { command, flags } = normalizeArgs(argv);
|
|
67
|
+
const translator = createTranslator(resolveLocale(flags.lang));
|
|
68
|
+
flags.lang = translator.locale;
|
|
69
|
+
const selectedModules = selectModules(
|
|
70
|
+
modules,
|
|
71
|
+
command === 'report' ? undefined : command,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!selectedModules.length) {
|
|
75
|
+
stderr.write(`Unknown command: ${command}\n`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const report = await createDoctorReport({
|
|
80
|
+
modules: selectedModules,
|
|
81
|
+
context: flags,
|
|
82
|
+
onProgress(event) {
|
|
83
|
+
if (flags.json) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (event.type === 'module:start') {
|
|
88
|
+
const moduleLabel = event.titleKey
|
|
89
|
+
? translator.t(event.titleKey)
|
|
90
|
+
: translator.t(toModuleTitleKey(event.moduleId));
|
|
91
|
+
stdout.write(`${translator.t('ui.progress.runningChecks', { moduleId: moduleLabel })}\n`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const payload = flags.json || command === 'report'
|
|
97
|
+
? renderJsonReport(report, { ...flags, t: translator.t })
|
|
98
|
+
: renderHumanReport(report, { ...flags, t: translator.t });
|
|
99
|
+
|
|
100
|
+
stdout.write(payload);
|
|
101
|
+
if (!payload.endsWith('\n')) {
|
|
102
|
+
stdout.write('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return report.overallRiskLevel === 'high' ? 2 : 0;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cac } from 'cac';
|
|
4
|
+
|
|
5
|
+
import { buildCli } from './cli-app.js';
|
|
6
|
+
import { createTranslator, resolveLocale } from './i18n.js';
|
|
7
|
+
|
|
8
|
+
function parseRequestedLang(argv) {
|
|
9
|
+
const index = argv.indexOf('--lang');
|
|
10
|
+
if (index >= 0 && argv[index + 1]) {
|
|
11
|
+
return argv[index + 1];
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const translator = createTranslator(resolveLocale(parseRequestedLang(process.argv.slice(2))));
|
|
17
|
+
const cli = cac('cc-env-checker');
|
|
18
|
+
const app = buildCli();
|
|
19
|
+
|
|
20
|
+
function registerCommand(name) {
|
|
21
|
+
cli
|
|
22
|
+
.command(name, translator.t(`cli.description.${name}`))
|
|
23
|
+
.option('--verbose', translator.t('cli.option.verbose'))
|
|
24
|
+
.option('--json', translator.t('cli.option.json'))
|
|
25
|
+
.option('--lang <locale>', translator.t('cli.option.lang'))
|
|
26
|
+
.option('--no-remote', translator.t('cli.option.noRemote'))
|
|
27
|
+
.option('--timeout <ms>', translator.t('cli.option.timeout'), {
|
|
28
|
+
default: 4000,
|
|
29
|
+
})
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
const args = [name];
|
|
32
|
+
if (options.verbose) args.push('--verbose');
|
|
33
|
+
if (options.json) args.push('--json');
|
|
34
|
+
if (options.lang) args.push('--lang', String(options.lang));
|
|
35
|
+
if (options.remote === false) args.push('--no-remote');
|
|
36
|
+
if (options.timeout) args.push('--timeout', String(options.timeout));
|
|
37
|
+
const exitCode = await app.run(args);
|
|
38
|
+
process.exitCode = exitCode;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
registerCommand('doctor');
|
|
43
|
+
registerCommand('network');
|
|
44
|
+
registerCommand('runtime');
|
|
45
|
+
registerCommand('install');
|
|
46
|
+
registerCommand('config');
|
|
47
|
+
registerCommand('report');
|
|
48
|
+
|
|
49
|
+
cli.help();
|
|
50
|
+
cli.parse();
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aggregateOverallRiskLevel,
|
|
3
|
+
buildCoverageMatrix,
|
|
4
|
+
buildRecommendedActions,
|
|
5
|
+
rankFindings,
|
|
6
|
+
summarizeEvidence,
|
|
7
|
+
summarizeModule,
|
|
8
|
+
} from './report.js';
|
|
9
|
+
|
|
10
|
+
export async function createDoctorReport({ modules, context, onProgress }) {
|
|
11
|
+
const moduleReports = [];
|
|
12
|
+
const allChecks = [];
|
|
13
|
+
|
|
14
|
+
for (const module of modules) {
|
|
15
|
+
onProgress?.({
|
|
16
|
+
type: 'module:start',
|
|
17
|
+
moduleId: module.id,
|
|
18
|
+
title: module.title,
|
|
19
|
+
...(module.titleKey ? { titleKey: module.titleKey } : {}),
|
|
20
|
+
});
|
|
21
|
+
const checks = await module.run(context);
|
|
22
|
+
const moduleSummary = summarizeModule(module.id, checks);
|
|
23
|
+
moduleReports.push({
|
|
24
|
+
id: module.id,
|
|
25
|
+
title: module.title,
|
|
26
|
+
titleKey: module.titleKey,
|
|
27
|
+
...moduleSummary,
|
|
28
|
+
});
|
|
29
|
+
allChecks.push(...checks);
|
|
30
|
+
onProgress?.({
|
|
31
|
+
type: 'module:finish',
|
|
32
|
+
moduleId: module.id,
|
|
33
|
+
title: module.title,
|
|
34
|
+
...(module.titleKey ? { titleKey: module.titleKey } : {}),
|
|
35
|
+
riskLevel: moduleSummary.riskLevel,
|
|
36
|
+
status: moduleSummary.status,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const topFindings = rankFindings(allChecks).slice(0, 5);
|
|
41
|
+
const highRiskCount = allChecks.filter((check) => check.riskLevel === 'high').length;
|
|
42
|
+
const mediumRiskCount = allChecks.filter((check) => check.riskLevel === 'medium').length;
|
|
43
|
+
const actionableCount = allChecks.filter((check) => check.status === 'warn' || check.status === 'fail').length;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
generatedAt: new Date().toISOString(),
|
|
47
|
+
overallRiskLevel: aggregateOverallRiskLevel(allChecks),
|
|
48
|
+
summary: {
|
|
49
|
+
moduleCount: moduleReports.length,
|
|
50
|
+
checkCount: allChecks.length,
|
|
51
|
+
highRiskCount,
|
|
52
|
+
mediumRiskCount,
|
|
53
|
+
actionableCount,
|
|
54
|
+
},
|
|
55
|
+
modules: moduleReports,
|
|
56
|
+
topFindings,
|
|
57
|
+
recommendedActions: buildRecommendedActions(topFindings),
|
|
58
|
+
articleModel: 'claude-code-client-risk-v1',
|
|
59
|
+
evidenceSummary: summarizeEvidence(allChecks),
|
|
60
|
+
coverageMatrix: buildCoverageMatrix(allChecks),
|
|
61
|
+
context: {
|
|
62
|
+
remote: context.remote,
|
|
63
|
+
timeoutMs: context.timeoutMs,
|
|
64
|
+
verbose: context.verbose,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|