@blamejs/exceptd-skills 0.16.28 → 0.16.30

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.
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * scripts/sync-package-description.js
5
+ *
6
+ * Regenerate the count-bearing tokens embedded in package.json.description from
7
+ * the live catalogs + manifest, so the description stays in sync when an
8
+ * auto-refresh changes an entry count. refresh-sbom copies the description into
9
+ * sbom.cdx.json, and check-sbom-currency validates every token against the live
10
+ * counts — without this sync, the first refresh that changes a count would fail
11
+ * the SBOM description-token gate on the auto-PR.
12
+ *
13
+ * Targeted, format-preserving: replaces only the integer in each known
14
+ * "<N> <label>" token (skills / catalogs / jurisdictions / per-catalog entry
15
+ * counts). Reuses check-sbom-currency's token table so the two can't drift.
16
+ *
17
+ * Run before refresh-sbom in the refresh apply path (and idempotent locally).
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const { DESCRIPTION_ENTRY_TOKENS, catalogEntryCount } = require('./check-sbom-currency');
24
+
25
+ function syncPackageDescription(root = path.join(__dirname, '..')) {
26
+ const pkgPath = path.join(root, 'package.json');
27
+ const dataDir = path.join(root, 'data');
28
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
29
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'));
30
+
31
+ const before = pkg.description || '';
32
+ let desc = before;
33
+
34
+ const liveSkills = Array.isArray(manifest.skills) ? manifest.skills.length : 0;
35
+ const liveCatalogs = fs.readdirSync(dataDir).filter((f) => f.endsWith('.json')).length;
36
+ let liveJurisdictions = null;
37
+ try {
38
+ const gf = JSON.parse(fs.readFileSync(path.join(dataDir, 'global-frameworks.json'), 'utf8'));
39
+ liveJurisdictions = Object.keys(gf).filter((k) => !k.startsWith('_')).length;
40
+ } catch { /* leave null — skip the jurisdiction token */ }
41
+
42
+ // Replace only the integer in "<N> <label>"; `labelRe` is the same (already
43
+ // regex-escaped) pattern check-sbom-currency matches, and $2 preserves the
44
+ // matched label text verbatim.
45
+ const sub = (n, labelRe) => {
46
+ if (n == null) return;
47
+ desc = desc.replace(new RegExp('(\\d+)(\\s+' + labelRe + '\\b)'), String(n) + '$2');
48
+ };
49
+
50
+ sub(liveSkills, 'skills');
51
+ sub(liveCatalogs, 'catalogs?');
52
+ sub(liveJurisdictions, 'jurisdictions?');
53
+ for (const { file, label } of DESCRIPTION_ENTRY_TOKENS) {
54
+ sub(catalogEntryCount(dataDir, file), label);
55
+ }
56
+
57
+ const changed = desc !== before;
58
+ if (changed) {
59
+ pkg.description = desc;
60
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
61
+ }
62
+ return { changed, description: desc };
63
+ }
64
+
65
+ if (require.main === module) {
66
+ const r = syncPackageDescription();
67
+ process.stdout.write(
68
+ r.changed
69
+ ? `package.json description synced from live counts:\n ${r.description}\n`
70
+ : 'package.json description already in sync with live counts.\n'
71
+ );
72
+ }
73
+
74
+ module.exports = { syncPackageDescription };
@@ -119,9 +119,15 @@ module.exports = {
119
119
  const ROOT = path.resolve(__dirname, "..");
120
120
 
121
121
  function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
122
+ // Sentinel thrown by fail() so the script body's try/finally still runs its
123
+ // temp-dir cleanup. process.exit() would preempt the finally, leaking the
124
+ // npm-pack temp dir on every run (predeploy gate + `npm test`). Abort by
125
+ // throwing instead and set the exit code via process.exitCode.
126
+ const ABORT = Symbol("verify-shipped-tarball:abort");
122
127
  function fail(msg, code = 1) {
123
128
  process.stderr.write(`[verify-shipped-tarball] FAIL: ${msg}\n`);
124
- process.exit(code);
129
+ process.exitCode = code;
130
+ throw ABORT;
125
131
  }
126
132
 
127
133
  // Gate the script body behind require.main === module so tests can
@@ -374,15 +380,20 @@ try {
374
380
  emit(`tarball verify result: ${pass}/${total} pass, ${fail_count} fail, ${miss} missing`);
375
381
  if (fail_count === 0 && miss === 0 && pass === total) {
376
382
  emit(`PASS — shipped tarball is internally consistent`);
377
- process.exit(0);
383
+ process.exitCode = 0;
384
+ } else {
385
+ for (const f of failures.slice(0, 10)) emit(` - ${f}`);
386
+ if (failures.length > 10) emit(` ... and ${failures.length - 10} more`);
387
+ emit(`FAIL — shipped tarball would be broken on every fresh install. Refusing to publish.`);
388
+ process.exitCode = 1;
378
389
  }
379
- for (const f of failures.slice(0, 10)) emit(` - ${f}`);
380
- if (failures.length > 10) emit(` ... and ${failures.length - 10} more`);
381
- emit(`FAIL shipped tarball would be broken on every fresh install. Refusing to publish.`);
382
- process.exit(1);
390
+ } catch (e) {
391
+ // ABORT is the fail() sentinel cleanup still runs via finally below. Any
392
+ // other error is unexpected: let finally run, then re-propagate it.
393
+ if (e !== ABORT) throw e;
383
394
  } finally {
384
395
  // Best-effort cleanup; leave on failure for diagnostics.
385
- if (process.exitCode === 0) {
396
+ if (process.exitCode === 0 || process.exitCode === undefined) {
386
397
  try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
387
398
  } else {
388
399
  emit(`temp dir preserved for inspection: ${tmpRoot}`);
@@ -39,7 +39,7 @@ const KEV_FEED = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited
39
39
  const EPSS_API = 'https://api.first.org/data/v1/epss?cve=';
40
40
  const REQUEST_TIMEOUT_MS = 10_000;
41
41
 
42
- const { selectNvdCvss } = require('../../lib/cvss');
42
+ const { selectNvdCvss, cvssVersionOf } = require('../../lib/cvss');
43
43
  const EPSS_DRIFT_THRESHOLD = 0.05; // |Δscore| or |Δpercentile| > 0.05 flags drift
44
44
  const USER_AGENT = 'exceptd-security/cve-validator (+https://exceptd.com)';
45
45
 
@@ -269,13 +269,23 @@ async function validateCve(cveId, localEntry) {
269
269
  }
270
270
 
271
271
  // --- Compare CVSS (only if NVD reachable & has data) ---
272
- if (cveFoundInNvd && fetched.cvss_score !== null && local.cvss_score !== null) {
273
- if (Math.abs(fetched.cvss_score - local.cvss_score) > 0.05) {
272
+ // Guard against cross-version downgrades: NVD often carries only a legacy v2
273
+ // metric for older CVEs while the catalog is curated to CVSS:3.1. Surfacing
274
+ // (and, on `refresh --apply`, writing) the lower v2 score/vector over a
275
+ // curated v3.x value is a downgrade, not a drift. Mirror the cache path's
276
+ // suppression (lib/refresh-external.js nvdDiffFromCache) so the live and
277
+ // cache refresh paths converge; a same-version re-score still flows through.
278
+ const localCvssVersion = cvssVersionOf(local.cvss_vector);
279
+ const fetchedCvssVersion = cvssVersionOf(fetched.cvss_vector);
280
+ const cvssIsDowngrade =
281
+ fetchedCvssVersion != null && localCvssVersion != null && fetchedCvssVersion < localCvssVersion;
282
+ if (cveFoundInNvd && !cvssIsDowngrade) {
283
+ if (fetched.cvss_score !== null && local.cvss_score !== null &&
284
+ Math.abs(fetched.cvss_score - local.cvss_score) > 0.05) {
274
285
  pushDiscrepancy(discrepancies, 'cvss_score', local.cvss_score, fetched.cvss_score, 'high');
275
286
  }
276
- }
277
- if (cveFoundInNvd && fetched.cvss_vector && local.cvss_vector) {
278
- if (fetched.cvss_vector !== local.cvss_vector) {
287
+ if (fetched.cvss_vector && local.cvss_vector &&
288
+ fetched.cvss_vector !== local.cvss_vector) {
279
289
  pushDiscrepancy(discrepancies, 'cvss_vector', local.cvss_vector, fetched.cvss_vector, 'medium');
280
290
  }
281
291
  }