vuln-scan 0.1.2 → 0.1.3
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 +10 -0
- package/cli.js +10 -0
- package/package.json +1 -1
- package/src/core.js +61 -2
package/README.md
CHANGED
|
@@ -27,6 +27,12 @@ A Node.js CLI that scans a project’s **lockfile** (npm / pnpm / yarn) to find
|
|
|
27
27
|
npx vuln-scan
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
### Run with pnpm
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm dlx vuln-scan
|
|
34
|
+
```
|
|
35
|
+
|
|
30
36
|
### Install globally
|
|
31
37
|
|
|
32
38
|
```bash
|
|
@@ -62,4 +68,8 @@ node ./cli.js
|
|
|
62
68
|
node ./cli.js --json
|
|
63
69
|
```
|
|
64
70
|
|
|
71
|
+
## Security
|
|
72
|
+
|
|
73
|
+
See [SECURITY.md](SECURITY.md) for vulnerability reporting and PGP details.
|
|
74
|
+
|
|
65
75
|
|
package/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { setDefaultResultOrder } from 'node:dns';
|
|
4
|
+
|
|
3
5
|
import chalk from 'chalk';
|
|
4
6
|
import ora from 'ora';
|
|
5
7
|
import Table from 'cli-table3';
|
|
@@ -40,6 +42,14 @@ function severityColor(sev) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
async function main() {
|
|
45
|
+
// Some environments have broken/blocked IPv6 routes. Node's fetch (undici) may try IPv6 first
|
|
46
|
+
// and time out even when IPv4 works (e.g., curl succeeds). Prefer IPv4 DNS results first.
|
|
47
|
+
try {
|
|
48
|
+
setDefaultResultOrder('ipv4first');
|
|
49
|
+
} catch {
|
|
50
|
+
// Older Node versions may not support this; best-effort.
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
const args = new Set(process.argv.slice(2));
|
|
44
54
|
if (args.has('--help') || args.has('-h')) {
|
|
45
55
|
printHelp();
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -320,10 +320,12 @@ async function queryOsv({ name, version }) {
|
|
|
320
320
|
}
|
|
321
321
|
};
|
|
322
322
|
|
|
323
|
-
const res = await
|
|
323
|
+
const res = await fetchWithRetry(OSV_QUERY_URL, {
|
|
324
324
|
method: 'POST',
|
|
325
325
|
headers: {
|
|
326
|
-
'content-type': 'application/json'
|
|
326
|
+
'content-type': 'application/json',
|
|
327
|
+
// A friendly UA can help with debugging/telemetry on the server side.
|
|
328
|
+
'user-agent': 'vuln-scan (node)'
|
|
327
329
|
},
|
|
328
330
|
body: JSON.stringify(body)
|
|
329
331
|
});
|
|
@@ -336,6 +338,63 @@ async function queryOsv({ name, version }) {
|
|
|
336
338
|
return res.json();
|
|
337
339
|
}
|
|
338
340
|
|
|
341
|
+
async function fetchWithRetry(url, init) {
|
|
342
|
+
const retries = 3;
|
|
343
|
+
const timeoutMs = 15_000;
|
|
344
|
+
|
|
345
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
346
|
+
const controller = new AbortController();
|
|
347
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
// eslint-disable-next-line no-undef
|
|
351
|
+
return await fetch(url, {
|
|
352
|
+
...init,
|
|
353
|
+
signal: controller.signal
|
|
354
|
+
});
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const isLast = attempt === retries;
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
const code = extractNodeErrorCode(err);
|
|
359
|
+
|
|
360
|
+
if (isLast) {
|
|
361
|
+
// Provide a more actionable message than the generic "fetch failed".
|
|
362
|
+
if (code === 'ETIMEDOUT' || code === 'ENETUNREACH' || code === 'EAI_AGAIN') {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Network error reaching OSV.dev (${code}). ` +
|
|
365
|
+
`If you're on an IPv6-restricted network, try: NODE_OPTIONS=--dns-result-order=ipv4first vuln-scan`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`Failed to reach OSV.dev: ${message}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Retry transient network issues / timeouts.
|
|
372
|
+
if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ENETUNREACH' || code === 'EAI_AGAIN') {
|
|
373
|
+
await delay(250 * Math.pow(2, attempt));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// AbortError or other non-network errors should fail fast.
|
|
378
|
+
throw err;
|
|
379
|
+
} finally {
|
|
380
|
+
clearTimeout(timeout);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Unreachable.
|
|
385
|
+
throw new Error('Failed to reach OSV.dev.');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractNodeErrorCode(err) {
|
|
389
|
+
// Node/undici errors often have nested causes.
|
|
390
|
+
const code = err?.cause?.code || err?.code;
|
|
391
|
+
return typeof code === 'string' ? code : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function delay(ms) {
|
|
395
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
396
|
+
}
|
|
397
|
+
|
|
339
398
|
function normalizeOsvResponse({ dependency, response }) {
|
|
340
399
|
const vulns = Array.isArray(response?.vulns) ? response.vulns : [];
|
|
341
400
|
|