@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.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/bin/exceptd.js +251 -18
- package/data/_indexes/_meta.json +4 -3
- package/data/_indexes/jurisdiction-map.json +31 -158
- package/data/playbooks/crypto.json +6 -0
- package/lib/auto-discovery.js +8 -0
- package/lib/collectors/README.md +3 -2
- package/lib/collectors/library-author.js +26 -9
- package/lib/collectors/secrets.js +8 -1
- package/lib/cross-ref-api.js +96 -31
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +264 -52
- package/lib/prefetch.js +78 -6
- package/lib/refresh-external.js +106 -5
- package/lib/scoring.js +49 -5
- package/lib/validate-cve-catalog.js +14 -2
- package/lib/validate-indexes.js +5 -0
- package/lib/validate-playbooks.js +133 -38
- package/manifest.json +53 -53
- package/orchestrator/pipeline.js +16 -4
- package/package.json +1 -1
- package/sbom.cdx.json +73 -58
- package/scripts/build-indexes.js +12 -1
- package/scripts/check-sbom-currency.js +76 -14
- package/scripts/refresh-sbom.js +1 -1
- package/scripts/run-e2e-scenarios.js +41 -11
- package/scripts/sync-package-description.js +74 -0
- package/scripts/verify-shipped-tarball.js +18 -7
- package/sources/validators/cve-validator.js +16 -6
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
}
|