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,443 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCheckResult,
|
|
3
|
+
fetchJson as defaultFetchJson,
|
|
4
|
+
fetchText as defaultFetchText,
|
|
5
|
+
proxyEnvSnapshot as defaultProxyEnvSnapshot,
|
|
6
|
+
resolveHost as defaultResolveHost,
|
|
7
|
+
} from './helpers.js';
|
|
8
|
+
|
|
9
|
+
const CONNECTIVITY_URL = 'https://api.ipify.org?format=json';
|
|
10
|
+
const GEO_URL = 'https://ipapi.co/json/';
|
|
11
|
+
const EXTERNAL_IP_SIGNAL_URL = 'https://ping0.cc/geo';
|
|
12
|
+
const TARGET_HOSTS = ['api.anthropic.com', 'console.anthropic.com'];
|
|
13
|
+
|
|
14
|
+
function createRemoteDisabledCheck(id, title, suggestion) {
|
|
15
|
+
return createCheckResult({
|
|
16
|
+
id,
|
|
17
|
+
titleKey: title,
|
|
18
|
+
status: 'skip',
|
|
19
|
+
riskLevel: 'unknown',
|
|
20
|
+
evidenceType: 'unknown',
|
|
21
|
+
source: 'remote',
|
|
22
|
+
summaryKey: 'check.network.remoteDisabled',
|
|
23
|
+
suggestionKey: suggestion,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function checkConnectivity(context, fetchJson) {
|
|
28
|
+
if (!context.remote) {
|
|
29
|
+
return createRemoteDisabledCheck(
|
|
30
|
+
'network.connectivity',
|
|
31
|
+
'check.network.connectivity.title',
|
|
32
|
+
'check.network.connectivity.suggestion.disabled',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ipResult = await fetchJson(CONNECTIVITY_URL, context.timeoutMs);
|
|
37
|
+
if (!ipResult.ok || !ipResult.data?.ip) {
|
|
38
|
+
return createCheckResult({
|
|
39
|
+
id: 'network.connectivity',
|
|
40
|
+
titleKey: 'check.network.connectivity.title',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
riskLevel: 'high',
|
|
43
|
+
evidenceType: 'observed',
|
|
44
|
+
source: 'remote',
|
|
45
|
+
summaryKey: 'check.network.connectivity.summary.fail',
|
|
46
|
+
details: [ipResult.error ?? `status=${ipResult.status}`],
|
|
47
|
+
suggestionKey: 'check.network.connectivity.suggestion.fail',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return createCheckResult({
|
|
52
|
+
id: 'network.connectivity',
|
|
53
|
+
titleKey: 'check.network.connectivity.title',
|
|
54
|
+
status: 'pass',
|
|
55
|
+
riskLevel: 'low',
|
|
56
|
+
evidenceType: 'observed',
|
|
57
|
+
source: 'remote',
|
|
58
|
+
summaryKey: 'check.network.connectivity.summary.pass',
|
|
59
|
+
details: [`ip=${ipResult.data.ip}`],
|
|
60
|
+
suggestionKey: 'check.common.noAction',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkEgressProfile(context, fetchJson) {
|
|
65
|
+
if (!context.remote) {
|
|
66
|
+
return createRemoteDisabledCheck(
|
|
67
|
+
'network.egress-profile',
|
|
68
|
+
'check.network.egress.title',
|
|
69
|
+
'check.network.egress.suggestion.disabled',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ipResult = await fetchJson(CONNECTIVITY_URL, context.timeoutMs);
|
|
74
|
+
if (!ipResult.ok || !ipResult.data?.ip) {
|
|
75
|
+
return createCheckResult({
|
|
76
|
+
id: 'network.egress-profile',
|
|
77
|
+
titleKey: 'check.network.egress.title',
|
|
78
|
+
status: 'warn',
|
|
79
|
+
riskLevel: 'medium',
|
|
80
|
+
evidenceType: 'observed',
|
|
81
|
+
source: 'remote',
|
|
82
|
+
summaryKey: 'check.network.egress.summary.fail',
|
|
83
|
+
details: [ipResult.error ?? `status=${ipResult.status}`],
|
|
84
|
+
suggestionKey: 'check.network.egress.suggestion.fail',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const geoResult = await fetchJson(GEO_URL, context.timeoutMs);
|
|
89
|
+
const details = [`ip=${ipResult.data.ip}`];
|
|
90
|
+
|
|
91
|
+
if (geoResult.ok && geoResult.data) {
|
|
92
|
+
details.push(`country=${geoResult.data.country_name ?? 'unknown'}`);
|
|
93
|
+
details.push(`region=${geoResult.data.region ?? 'unknown'}`);
|
|
94
|
+
details.push(`city=${geoResult.data.city ?? 'unknown'}`);
|
|
95
|
+
details.push(`asn=${geoResult.data.asn ?? 'unknown'}`);
|
|
96
|
+
details.push(`org=${geoResult.data.org ?? 'unknown'}`);
|
|
97
|
+
} else {
|
|
98
|
+
return createCheckResult({
|
|
99
|
+
id: 'network.egress-profile',
|
|
100
|
+
titleKey: 'check.network.egress.title',
|
|
101
|
+
status: 'warn',
|
|
102
|
+
riskLevel: 'medium',
|
|
103
|
+
evidenceType: 'observed',
|
|
104
|
+
source: 'remote',
|
|
105
|
+
summaryKey: 'check.network.egress.summary.partial',
|
|
106
|
+
details,
|
|
107
|
+
suggestionKey: 'check.network.egress.suggestion.partial',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return createCheckResult({
|
|
112
|
+
id: 'network.egress-profile',
|
|
113
|
+
titleKey: 'check.network.egress.title',
|
|
114
|
+
status: 'pass',
|
|
115
|
+
riskLevel: 'low',
|
|
116
|
+
evidenceType: 'observed',
|
|
117
|
+
source: 'remote',
|
|
118
|
+
summaryKey: 'check.network.egress.summary.pass',
|
|
119
|
+
details,
|
|
120
|
+
suggestionKey: 'check.network.egress.suggestion.pass',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function inferExternalIpRisk(signal) {
|
|
125
|
+
const normalizedRisk = String(signal.risk ?? signal.risk_level ?? '').toLowerCase();
|
|
126
|
+
const numericRiskScore = Number(signal.risk_score);
|
|
127
|
+
const strongFlags = [
|
|
128
|
+
Boolean(signal.is_proxy),
|
|
129
|
+
Boolean(signal.is_vpn),
|
|
130
|
+
Boolean(signal.is_datacenter),
|
|
131
|
+
].filter(Boolean).length;
|
|
132
|
+
|
|
133
|
+
if (normalizedRisk === 'high' || Number.isFinite(numericRiskScore) && numericRiskScore >= 80 || strongFlags >= 2) {
|
|
134
|
+
return 'high';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
normalizedRisk === 'medium' ||
|
|
139
|
+
Number.isFinite(numericRiskScore) && numericRiskScore >= 40 ||
|
|
140
|
+
strongFlags === 1
|
|
141
|
+
) {
|
|
142
|
+
return 'medium';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
normalizedRisk === 'low' ||
|
|
147
|
+
Number.isFinite(numericRiskScore) ||
|
|
148
|
+
signal.ip_type ||
|
|
149
|
+
signal.asn ||
|
|
150
|
+
signal.org
|
|
151
|
+
) {
|
|
152
|
+
return 'low';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return 'unknown';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parsePing0GeoText(text) {
|
|
159
|
+
const lines = String(text)
|
|
160
|
+
.split(/\r?\n/)
|
|
161
|
+
.map((line) => line.trim())
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
|
|
164
|
+
if (lines.length < 4) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
ip: lines[0],
|
|
170
|
+
location: lines[1],
|
|
171
|
+
asn: lines[2],
|
|
172
|
+
org: lines[3],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function checkExternalIpSignal(context, fetchJson, fetchText) {
|
|
177
|
+
if (!context.remote) {
|
|
178
|
+
return createRemoteDisabledCheck(
|
|
179
|
+
'network.external-ip-signal',
|
|
180
|
+
'check.network.externalIpSignal.title',
|
|
181
|
+
'check.network.externalIpSignal.suggestion.disabled',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const textResult = await fetchText(EXTERNAL_IP_SIGNAL_URL, context.timeoutMs);
|
|
186
|
+
if (textResult.ok) {
|
|
187
|
+
const parsed = parsePing0GeoText(textResult.text);
|
|
188
|
+
if (parsed) {
|
|
189
|
+
return createCheckResult({
|
|
190
|
+
id: 'network.external-ip-signal',
|
|
191
|
+
titleKey: 'check.network.externalIpSignal.title',
|
|
192
|
+
status: 'pass',
|
|
193
|
+
riskLevel: 'low',
|
|
194
|
+
evidenceType: 'observed',
|
|
195
|
+
source: 'remote',
|
|
196
|
+
summaryKey: 'check.network.externalIpSignal.summary.low',
|
|
197
|
+
details: [
|
|
198
|
+
`ip=${parsed.ip}`,
|
|
199
|
+
`location=${parsed.location}`,
|
|
200
|
+
`asn=${parsed.asn}`,
|
|
201
|
+
`org=${parsed.org}`,
|
|
202
|
+
],
|
|
203
|
+
suggestionKey: 'check.network.externalIpSignal.suggestion.low',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = await fetchJson(EXTERNAL_IP_SIGNAL_URL, context.timeoutMs);
|
|
209
|
+
if (!result.ok || !result.data) {
|
|
210
|
+
return createCheckResult({
|
|
211
|
+
id: 'network.external-ip-signal',
|
|
212
|
+
titleKey: 'check.network.externalIpSignal.title',
|
|
213
|
+
status: 'warn',
|
|
214
|
+
riskLevel: 'unknown',
|
|
215
|
+
evidenceType: 'unknown',
|
|
216
|
+
source: 'remote',
|
|
217
|
+
summaryKey: 'check.network.externalIpSignal.summary.unavailable',
|
|
218
|
+
details: [
|
|
219
|
+
textResult.ok ? 'ping0 text format was incomplete' : (textResult.error ?? `status=${textResult.status}`),
|
|
220
|
+
result.error ?? `status=${result.status}`,
|
|
221
|
+
],
|
|
222
|
+
suggestionKey: 'check.network.externalIpSignal.suggestion.unavailable',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const details = [];
|
|
227
|
+
for (const [key, value] of Object.entries({
|
|
228
|
+
risk: result.data.risk,
|
|
229
|
+
risk_score: result.data.risk_score,
|
|
230
|
+
is_proxy: result.data.is_proxy,
|
|
231
|
+
is_vpn: result.data.is_vpn,
|
|
232
|
+
is_datacenter: result.data.is_datacenter,
|
|
233
|
+
ip_type: result.data.ip_type,
|
|
234
|
+
asn: result.data.asn,
|
|
235
|
+
org: result.data.org,
|
|
236
|
+
})) {
|
|
237
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
238
|
+
details.push(`${key}=${value}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const inferredRisk = inferExternalIpRisk(result.data);
|
|
243
|
+
const status = inferredRisk === 'low' ? 'pass' : 'warn';
|
|
244
|
+
const summaryKeyByRisk = {
|
|
245
|
+
low: 'check.network.externalIpSignal.summary.low',
|
|
246
|
+
medium: 'check.network.externalIpSignal.summary.medium',
|
|
247
|
+
high: 'check.network.externalIpSignal.summary.high',
|
|
248
|
+
unknown: 'check.network.externalIpSignal.summary.unavailable',
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return createCheckResult({
|
|
252
|
+
id: 'network.external-ip-signal',
|
|
253
|
+
titleKey: 'check.network.externalIpSignal.title',
|
|
254
|
+
status,
|
|
255
|
+
riskLevel: inferredRisk,
|
|
256
|
+
evidenceType: inferredRisk === 'unknown' ? 'unknown' : 'inferred',
|
|
257
|
+
source: 'remote',
|
|
258
|
+
summaryKey: summaryKeyByRisk[inferredRisk],
|
|
259
|
+
details,
|
|
260
|
+
suggestionKey:
|
|
261
|
+
inferredRisk === 'low'
|
|
262
|
+
? 'check.network.externalIpSignal.suggestion.low'
|
|
263
|
+
: 'check.network.externalIpSignal.suggestion.review',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function checkDnsBaseline(resolveHost) {
|
|
268
|
+
const resolutions = await Promise.all(TARGET_HOSTS.map((hostname) => resolveHost(hostname)));
|
|
269
|
+
const details = [];
|
|
270
|
+
let hasFailure = false;
|
|
271
|
+
|
|
272
|
+
resolutions.forEach((resolution, index) => {
|
|
273
|
+
const hostname = TARGET_HOSTS[index];
|
|
274
|
+
if (!resolution.ok || (!resolution.v4.length && !resolution.v6.length)) {
|
|
275
|
+
hasFailure = true;
|
|
276
|
+
details.push(`${hostname}=unresolved`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
details.push(`${hostname}.ipv4=${resolution.v4.length}`);
|
|
281
|
+
details.push(`${hostname}.ipv6=${resolution.v6.length}`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (hasFailure) {
|
|
285
|
+
return createCheckResult({
|
|
286
|
+
id: 'network.dns-baseline',
|
|
287
|
+
titleKey: 'check.network.dnsBaseline.title',
|
|
288
|
+
status: 'fail',
|
|
289
|
+
riskLevel: 'high',
|
|
290
|
+
evidenceType: 'observed',
|
|
291
|
+
source: 'mixed',
|
|
292
|
+
summaryKey: 'check.network.dnsBaseline.summary.fail',
|
|
293
|
+
details,
|
|
294
|
+
suggestionKey: 'check.network.dnsBaseline.suggestion.fail',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return createCheckResult({
|
|
299
|
+
id: 'network.dns-baseline',
|
|
300
|
+
titleKey: 'check.network.dnsBaseline.title',
|
|
301
|
+
status: 'pass',
|
|
302
|
+
riskLevel: 'low',
|
|
303
|
+
evidenceType: 'observed',
|
|
304
|
+
source: 'mixed',
|
|
305
|
+
summaryKey: 'check.network.dnsBaseline.summary.pass',
|
|
306
|
+
details,
|
|
307
|
+
suggestionKey: 'check.common.noAction',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function checkDnsFamily(resolveHost) {
|
|
312
|
+
const resolutions = await Promise.all(TARGET_HOSTS.map((hostname) => resolveHost(hostname)));
|
|
313
|
+
const details = [];
|
|
314
|
+
let partial = false;
|
|
315
|
+
let knownHostCount = 0;
|
|
316
|
+
|
|
317
|
+
resolutions.forEach((resolution, index) => {
|
|
318
|
+
const hostname = TARGET_HOSTS[index];
|
|
319
|
+
if (!resolution.ok || (!resolution.v4.length && !resolution.v6.length)) {
|
|
320
|
+
details.push(`${hostname}=unknown`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
knownHostCount += 1;
|
|
325
|
+
if (!resolution.v4.length || !resolution.v6.length) {
|
|
326
|
+
partial = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
details.push(`${hostname}.ipv4=${resolution.v4.length}`);
|
|
330
|
+
details.push(`${hostname}.ipv6=${resolution.v6.length}`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (knownHostCount === 0) {
|
|
334
|
+
return createCheckResult({
|
|
335
|
+
id: 'network.dns-family',
|
|
336
|
+
titleKey: 'check.network.dnsFamily.title',
|
|
337
|
+
status: 'skip',
|
|
338
|
+
riskLevel: 'unknown',
|
|
339
|
+
evidenceType: 'unknown',
|
|
340
|
+
source: 'mixed',
|
|
341
|
+
summaryKey: 'check.network.dnsFamily.summary.skip',
|
|
342
|
+
details,
|
|
343
|
+
suggestionKey: 'check.network.dnsFamily.suggestion.skip',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (partial) {
|
|
348
|
+
return createCheckResult({
|
|
349
|
+
id: 'network.dns-family',
|
|
350
|
+
titleKey: 'check.network.dnsFamily.title',
|
|
351
|
+
status: 'warn',
|
|
352
|
+
riskLevel: 'medium',
|
|
353
|
+
evidenceType: 'observed',
|
|
354
|
+
source: 'mixed',
|
|
355
|
+
summaryKey: 'check.network.dnsFamily.summary.warn',
|
|
356
|
+
details,
|
|
357
|
+
suggestionKey: 'check.network.dnsFamily.suggestion.warn',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return createCheckResult({
|
|
362
|
+
id: 'network.dns-family',
|
|
363
|
+
titleKey: 'check.network.dnsFamily.title',
|
|
364
|
+
status: 'pass',
|
|
365
|
+
riskLevel: 'low',
|
|
366
|
+
evidenceType: 'observed',
|
|
367
|
+
source: 'mixed',
|
|
368
|
+
summaryKey: 'check.network.dnsFamily.summary.pass',
|
|
369
|
+
details,
|
|
370
|
+
suggestionKey: 'check.common.noAction',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function checkProxyConsistency(proxyEnvSnapshot) {
|
|
375
|
+
const details = proxyEnvSnapshot();
|
|
376
|
+
|
|
377
|
+
if (!details.length) {
|
|
378
|
+
return createCheckResult({
|
|
379
|
+
id: 'network.proxy-env',
|
|
380
|
+
titleKey: 'check.network.proxy.title',
|
|
381
|
+
status: 'pass',
|
|
382
|
+
riskLevel: 'low',
|
|
383
|
+
evidenceType: 'observed',
|
|
384
|
+
source: 'local',
|
|
385
|
+
summaryKey: 'check.network.proxy.summary.unset',
|
|
386
|
+
details: ['proxy=unset'],
|
|
387
|
+
suggestionKey: 'check.network.proxy.suggestion.unset',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const hasHttp = details.some((entry) => entry.startsWith('HTTP_PROXY='));
|
|
392
|
+
const hasHttps = details.some((entry) => entry.startsWith('HTTPS_PROXY='));
|
|
393
|
+
|
|
394
|
+
if (hasHttp !== hasHttps) {
|
|
395
|
+
return createCheckResult({
|
|
396
|
+
id: 'network.proxy-env',
|
|
397
|
+
titleKey: 'check.network.proxy.title',
|
|
398
|
+
status: 'warn',
|
|
399
|
+
riskLevel: 'medium',
|
|
400
|
+
evidenceType: 'observed',
|
|
401
|
+
source: 'local',
|
|
402
|
+
summaryKey: 'check.network.proxy.summary.warn',
|
|
403
|
+
details,
|
|
404
|
+
suggestionKey: 'check.network.proxy.suggestion.warn',
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return createCheckResult({
|
|
409
|
+
id: 'network.proxy-env',
|
|
410
|
+
titleKey: 'check.network.proxy.title',
|
|
411
|
+
status: 'pass',
|
|
412
|
+
riskLevel: 'low',
|
|
413
|
+
evidenceType: 'observed',
|
|
414
|
+
source: 'local',
|
|
415
|
+
summaryKey: 'check.network.proxy.summary.pass',
|
|
416
|
+
details,
|
|
417
|
+
suggestionKey: 'check.network.proxy.suggestion.pass',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function createNetworkModule({
|
|
422
|
+
fetchJson = defaultFetchJson,
|
|
423
|
+
fetchText = defaultFetchText,
|
|
424
|
+
resolveHost = defaultResolveHost,
|
|
425
|
+
proxyEnvSnapshot = defaultProxyEnvSnapshot,
|
|
426
|
+
} = {}) {
|
|
427
|
+
return {
|
|
428
|
+
id: 'network',
|
|
429
|
+
titleKey: 'module.network',
|
|
430
|
+
async run(context) {
|
|
431
|
+
return [
|
|
432
|
+
await checkConnectivity(context, fetchJson),
|
|
433
|
+
await checkEgressProfile(context, fetchJson),
|
|
434
|
+
await checkExternalIpSignal(context, fetchJson, fetchText),
|
|
435
|
+
await checkDnsBaseline(resolveHost),
|
|
436
|
+
await checkDnsFamily(resolveHost),
|
|
437
|
+
checkProxyConsistency(proxyEnvSnapshot),
|
|
438
|
+
];
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export const networkModule = createNetworkModule();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createCheckResult, runtimeSnapshot, tryExec } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
async function checkNodeRuntime() {
|
|
4
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
5
|
+
const details = runtimeSnapshot();
|
|
6
|
+
|
|
7
|
+
if (major < 20) {
|
|
8
|
+
return createCheckResult({
|
|
9
|
+
id: 'runtime.node',
|
|
10
|
+
titleKey: 'check.runtime.node.title',
|
|
11
|
+
status: 'fail',
|
|
12
|
+
riskLevel: 'high',
|
|
13
|
+
evidenceType: 'observed',
|
|
14
|
+
source: 'local',
|
|
15
|
+
summaryKey: 'check.runtime.node.summary.fail',
|
|
16
|
+
details,
|
|
17
|
+
suggestionKey: 'check.runtime.node.suggestion.fail',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return createCheckResult({
|
|
22
|
+
id: 'runtime.node',
|
|
23
|
+
titleKey: 'check.runtime.node.title',
|
|
24
|
+
status: 'pass',
|
|
25
|
+
riskLevel: 'low',
|
|
26
|
+
evidenceType: 'observed',
|
|
27
|
+
source: 'local',
|
|
28
|
+
summaryKey: 'check.runtime.node.summary.pass',
|
|
29
|
+
details,
|
|
30
|
+
suggestionKey: 'check.common.noAction',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function checkNpmAvailability() {
|
|
35
|
+
const result = await tryExec('npm', ['-v']);
|
|
36
|
+
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
return createCheckResult({
|
|
39
|
+
id: 'runtime.npm',
|
|
40
|
+
titleKey: 'check.runtime.npm.title',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
riskLevel: 'high',
|
|
43
|
+
evidenceType: 'observed',
|
|
44
|
+
source: 'local',
|
|
45
|
+
summaryKey: 'check.runtime.npm.summary.fail',
|
|
46
|
+
details: [result.message],
|
|
47
|
+
suggestionKey: 'check.runtime.npm.suggestion.fail',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return createCheckResult({
|
|
52
|
+
id: 'runtime.npm',
|
|
53
|
+
titleKey: 'check.runtime.npm.title',
|
|
54
|
+
status: 'pass',
|
|
55
|
+
riskLevel: 'low',
|
|
56
|
+
evidenceType: 'observed',
|
|
57
|
+
source: 'local',
|
|
58
|
+
summaryKey: 'check.runtime.npm.summary.pass',
|
|
59
|
+
details: [`npm=${result.stdout}`],
|
|
60
|
+
suggestionKey: 'check.common.noAction',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkPathHealth() {
|
|
65
|
+
const pathValue = process.env.PATH ?? '';
|
|
66
|
+
const segments = pathValue.split(':').filter(Boolean);
|
|
67
|
+
const duplicates = segments.filter((segment, index) => segments.indexOf(segment) !== index);
|
|
68
|
+
|
|
69
|
+
if (duplicates.length) {
|
|
70
|
+
return createCheckResult({
|
|
71
|
+
id: 'runtime.path',
|
|
72
|
+
titleKey: 'check.runtime.path.title',
|
|
73
|
+
status: 'warn',
|
|
74
|
+
riskLevel: 'medium',
|
|
75
|
+
evidenceType: 'observed',
|
|
76
|
+
source: 'local',
|
|
77
|
+
summaryKey: 'check.runtime.path.summary.warn',
|
|
78
|
+
details: duplicates.map((entry) => `duplicate=${entry}`),
|
|
79
|
+
suggestionKey: 'check.runtime.path.suggestion.warn',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return createCheckResult({
|
|
84
|
+
id: 'runtime.path',
|
|
85
|
+
titleKey: 'check.runtime.path.title',
|
|
86
|
+
status: 'pass',
|
|
87
|
+
riskLevel: 'low',
|
|
88
|
+
evidenceType: 'observed',
|
|
89
|
+
source: 'local',
|
|
90
|
+
summaryKey: 'check.runtime.path.summary.pass',
|
|
91
|
+
details: [`segments=${segments.length}`],
|
|
92
|
+
suggestionKey: 'check.common.noAction',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const runtimeModule = {
|
|
97
|
+
id: 'runtime',
|
|
98
|
+
titleKey: 'module.runtime',
|
|
99
|
+
async run() {
|
|
100
|
+
return [
|
|
101
|
+
await checkNodeRuntime(),
|
|
102
|
+
await checkNpmAvailability(),
|
|
103
|
+
await checkPathHealth(),
|
|
104
|
+
];
|
|
105
|
+
},
|
|
106
|
+
};
|