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.
Files changed (4) hide show
  1. package/README.md +10 -0
  2. package/cli.js +10 -0
  3. package/package.json +1 -1
  4. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vuln-scan",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Node.js CLI to scan dependency lockfiles for vulnerabilities using OSV.dev",
5
5
  "type": "module",
6
6
  "bin": {
package/src/core.js CHANGED
@@ -320,10 +320,12 @@ async function queryOsv({ name, version }) {
320
320
  }
321
321
  };
322
322
 
323
- const res = await fetch(OSV_QUERY_URL, {
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