@blamejs/exceptd-skills 0.19.32 → 0.19.34
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 +22 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +8 -8
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +60118 -0
- package/data/attack-techniques.json +267 -7
- package/data/cve-catalog.json +9991 -3
- package/data/cwe-catalog.json +109 -2
- package/data/framework-control-gaps.json +578 -3
- package/data/zeroday-lessons.json +8330 -1
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +2 -2
- package/sbom.cdx.json +253 -253
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* atlas-ttps, `skills_referencing` for the other three) listing every
|
|
11
|
-
* skill that points at that entry. The reverse field drifts whenever a
|
|
12
|
-
* skill adds or removes a forward ref without the catalog being updated
|
|
13
|
-
* in lockstep — this script rebuilds the reverse direction from the
|
|
14
|
-
* forward source of truth so the two never disagree.
|
|
15
|
-
*
|
|
16
|
-
* Behaviour. For each catalog file:
|
|
17
|
-
* 1. Walk every skill's relevant forward-ref array in manifest.json.
|
|
18
|
-
* 2. For every catalog entry, list every skill that references it.
|
|
19
|
-
* 3. Sort the resulting skill list and write it back into the per-entry
|
|
20
|
-
* reverse field. All other fields are preserved untouched.
|
|
21
|
-
*
|
|
22
|
-
* The script is idempotent: a second run produces no further changes.
|
|
23
|
-
*
|
|
24
|
-
* The script does NOT touch playbooks_referencing — that field carries
|
|
25
|
-
* playbook ids (data/playbooks/*.json), not skill names; it has its own
|
|
26
|
-
* source of truth and is out of scope for this refresh.
|
|
27
|
-
*
|
|
28
|
-
* Run: node scripts/refresh-reverse-refs.js
|
|
29
|
-
* npm run refresh-reverse-refs
|
|
30
|
-
*
|
|
31
|
-
* Exit code: 0 always (script is unconditionally write-mode). Use
|
|
32
|
-
* tests/reverse-ref-drift.test.js as the read-only drift detector.
|
|
3
|
+
* Rebuilds the denormalised reverse-reference fields in the data catalogs from the
|
|
4
|
+
* forward direction, which is the source of truth: a skill's atlas_refs / cwe_refs
|
|
5
|
+
* / d3fend_refs / rfc_refs, and a CVE's forward refs. Every other field is
|
|
6
|
+
* preserved and a second run changes nothing. `playbooks_referencing` is out of
|
|
7
|
+
* scope — it carries playbook ids, not skill names.
|
|
8
|
+
* Write-mode only, exit 0 always; the read-only drift detector is
|
|
9
|
+
* tests/reverse-ref-drift.test.js.
|
|
33
10
|
*/
|
|
34
11
|
|
|
35
12
|
'use strict';
|
|
@@ -43,17 +20,12 @@ const CVE_CATALOG_PATH = path.join(REPO_ROOT, 'data', 'cve-catalog.json');
|
|
|
43
20
|
const DATA_DIR = path.join(REPO_ROOT, 'data');
|
|
44
21
|
|
|
45
22
|
/* Per-catalog config:
|
|
46
|
-
* file
|
|
47
|
-
* forwardField
|
|
48
|
-
* reverseField
|
|
49
|
-
* source
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* entries so the reverse direction tracks operator-queryable
|
|
53
|
-
* truth, not in-progress curation state)
|
|
54
|
-
* entryKey field on the source object used as the reverse-list value
|
|
55
|
-
* ('name' for skills; '<self-id>' for CVE entries via the
|
|
56
|
-
* map key, so the helper substitutes the iterating key)
|
|
23
|
+
* file relative path under data/
|
|
24
|
+
* forwardField array (or dict, with forwardFieldShape) on the source object
|
|
25
|
+
* reverseField per-entry field this script overwrites in the catalog
|
|
26
|
+
* source 'manifest.skills' walks every skill; 'cve.entries' every
|
|
27
|
+
* non-draft CVE
|
|
28
|
+
* entryKey field used as the reverse-list value; null means the key itself
|
|
57
29
|
*/
|
|
58
30
|
const CATALOGS = [
|
|
59
31
|
{
|
|
@@ -84,12 +56,7 @@ const CATALOGS = [
|
|
|
84
56
|
source: 'manifest.skills',
|
|
85
57
|
entryKey: 'name',
|
|
86
58
|
},
|
|
87
|
-
//
|
|
88
|
-
// `evidence_cves` as the operator-facing "which CVEs land here" index;
|
|
89
|
-
// previously hand-maintained and drifted whenever a new CVE landed
|
|
90
|
-
// without the matching CWE's evidence_cves being updated. Now mirrors
|
|
91
|
-
// `cve.cwe_refs` → `cwe.evidence_cves` automatically. Drafts excluded
|
|
92
|
-
// (they're invisible to default consumers anyway).
|
|
59
|
+
// `evidence_cves` mirrors cve.cwe_refs: which CVEs land on this CWE.
|
|
93
60
|
{
|
|
94
61
|
file: 'cwe-catalog.json',
|
|
95
62
|
forwardField: 'cwe_refs',
|
|
@@ -97,14 +64,8 @@ const CATALOGS = [
|
|
|
97
64
|
source: 'cve.entries',
|
|
98
65
|
entryKey: null, // value is the iterating CVE id
|
|
99
66
|
},
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// by gap-id) and gap.evidence_cves (array of CVE ids). The forward
|
|
103
|
-
// shape on the CVE side is an OBJECT not an array — keys are the gap
|
|
104
|
-
// ids, values are per-CVE narrative. The reverse direction (which CVEs
|
|
105
|
-
// cite this gap) is a simple set of CVE ids on the gap entry. The
|
|
106
|
-
// helper handles the dict-keyed forward field via the
|
|
107
|
-
// `forwardFieldShape: 'object-keys'` flag.
|
|
67
|
+
// The forward side here is an OBJECT, not an array: keys are gap ids, values
|
|
68
|
+
// the per-CVE narrative, hence forwardFieldShape. The reverse is a set of ids.
|
|
108
69
|
{
|
|
109
70
|
file: 'framework-control-gaps.json',
|
|
110
71
|
forwardField: 'framework_control_gaps',
|
|
@@ -113,12 +74,7 @@ const CATALOGS = [
|
|
|
113
74
|
source: 'cve.entries',
|
|
114
75
|
entryKey: null, // value is the iterating CVE id
|
|
115
76
|
},
|
|
116
|
-
//
|
|
117
|
-
// technique gets a `cve_refs` array carrying the CVE ids that cite it.
|
|
118
|
-
// Pre-v0.13 these catalogs had only forward refs (CVE → TTP); operators
|
|
119
|
-
// reading an ATLAS / ATT&CK entry could not see which CVEs cite it
|
|
120
|
-
// without grepping the whole catalog. These back-edges make the
|
|
121
|
-
// relationship symmetric: a citation is readable from either end.
|
|
77
|
+
// `cve_refs` back-edges: an ATLAS or ATT&CK entry shows which CVEs cite it.
|
|
122
78
|
{
|
|
123
79
|
file: 'atlas-ttps.json',
|
|
124
80
|
forwardField: 'atlas_refs',
|
|
@@ -152,16 +108,9 @@ function buildReverseIndex(skills, forwardField) {
|
|
|
152
108
|
return index;
|
|
153
109
|
}
|
|
154
110
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// Draft entries are skipped — drafts are invisible to default consumers
|
|
159
|
-
// via cross-ref-api, so the reverse direction should track operator-
|
|
160
|
-
// queryable truth, not in-progress curation state.
|
|
161
|
-
//
|
|
162
|
-
// v0.12.40: forwardFieldShape parameter handles the
|
|
163
|
-
// CVE.framework_control_gaps case where the forward field is a dict
|
|
164
|
-
// (gap-id → narrative) rather than an array.
|
|
111
|
+
// catalogEntryId -> Set<cveId>. Drafts are skipped: they are invisible to default
|
|
112
|
+
// consumers through cross-ref-api. Pass forwardFieldShape 'object-keys' when the
|
|
113
|
+
// forward field is a dict.
|
|
165
114
|
function buildCveReverseIndex(cveCatalog, forwardField, forwardFieldShape) {
|
|
166
115
|
const index = new Map();
|
|
167
116
|
for (const [cveId, entry] of Object.entries(cveCatalog)) {
|
|
@@ -221,14 +170,9 @@ function rebuildCatalog(cfg, manifest, cveCatalog) {
|
|
|
221
170
|
}
|
|
222
171
|
}
|
|
223
172
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// failing gates for the two signals this script can surface live
|
|
228
|
-
// elsewhere: orphan forward refs are hard-errored by lib/lint-skills.js
|
|
229
|
-
// ref-resolution ("<id> not present in data/<catalog>"), and reverse-field
|
|
230
|
-
// drift is hard-failed by tests/reverse-ref-drift.test.js. Do not wire
|
|
231
|
-
// this script itself as a "reverse refs clean?" check — it always passes.
|
|
173
|
+
// Orphans — forward refs naming a catalog entry that does not exist — are
|
|
174
|
+
// reported, never fatal. Never wire this script up as a "reverse refs clean?"
|
|
175
|
+
// check: the failing gates are lib/lint-skills.js and reverse-ref-drift.test.js.
|
|
232
176
|
for (const id of index.keys()) {
|
|
233
177
|
if (!seenIds.has(id)) orphans.push(id);
|
|
234
178
|
}
|
|
@@ -249,15 +193,8 @@ function rebuildCatalog(cfg, manifest, cveCatalog) {
|
|
|
249
193
|
};
|
|
250
194
|
}
|
|
251
195
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
* `feeds_into[]` listing playbook ids it chains TO; `fed_by[]` is the
|
|
255
|
-
* symmetric "which playbooks chain into me." Pre-v0.13 operators reading
|
|
256
|
-
* a playbook couldn't see what fed it without grepping every other
|
|
257
|
-
* playbook's feeds_into. The reverse field lives on the playbook's
|
|
258
|
-
* top-level under `_meta.fed_by` (paired with the existing _meta.feeds_into
|
|
259
|
-
* for shape symmetry).
|
|
260
|
-
*/
|
|
196
|
+
// `_meta.fed_by` is the symmetric counterpart of `_meta.feeds_into`: which
|
|
197
|
+
// playbooks chain INTO this one. Writes each playbook file in place.
|
|
261
198
|
function rebuildPlaybookReverse() {
|
|
262
199
|
const playbooksDir = path.join(DATA_DIR, 'playbooks');
|
|
263
200
|
if (!fs.existsSync(playbooksDir)) return { file: 'playbooks/*.json', source: 'playbook.feeds_into', reverseField: 'fed_by', changed: 0, added: 0, removed: 0, unchanged: 0, orphans: [] };
|
|
@@ -271,12 +208,8 @@ function rebuildPlaybookReverse() {
|
|
|
271
208
|
if (!data || !data._meta || !data._meta.id) continue;
|
|
272
209
|
playbookEntries.push({ file: f, filePath, data });
|
|
273
210
|
}
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// The fed_by reverse is a simple array of source playbook ids (the
|
|
277
|
-
// condition is per-edge; preserved on feeds_into, not duplicated on
|
|
278
|
-
// fed_by — operators read fed_by to find candidates, then look at the
|
|
279
|
-
// source playbook's feeds_into for the gating condition).
|
|
211
|
+
// targetId -> Set<sourceId>. A feeds_into entry carries `playbook_id` +
|
|
212
|
+
// `condition`; fed_by keeps only the ids, so the condition is not duplicated.
|
|
280
213
|
const index = new Map();
|
|
281
214
|
for (const { data } of playbookEntries) {
|
|
282
215
|
const meta = data._meta;
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Thin per-type wrapper for the RFC refresher. Logic lives in
|
|
7
|
-
* scripts/refresh-upstream-catalogs.js#refreshRfc. Use this entry when
|
|
8
|
-
* you want to refresh only the RFC catalog without touching ATT&CK /
|
|
9
|
-
* ATLAS / D3FEND.
|
|
10
|
-
*
|
|
11
|
-
* node scripts/refresh-rfc-index.js [--dry-run]
|
|
12
|
-
*
|
|
13
|
-
* Wired as `npm run refresh-rfc-index`.
|
|
4
|
+
* Refreshes only the RFC catalog, leaving ATT&CK / ATLAS / D3FEND untouched.
|
|
5
|
+
* Logic lives in scripts/refresh-upstream-catalogs.js#refreshRfc.
|
|
14
6
|
*/
|
|
15
7
|
const { refreshRfc } = require("./refresh-upstream-catalogs.js");
|
|
16
8
|
const dry = process.argv.includes("--dry-run");
|
package/scripts/refresh-sbom.js
CHANGED
|
@@ -1,50 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* package.json `dependencies: {}`). The SBOM therefore documents the
|
|
7
|
-
* project as an application component with an empty `components` array
|
|
8
|
-
* and pulls live surface counts from manifest.json + data/*.json so the
|
|
9
|
-
* artifact never silently drifts when the surface changes.
|
|
10
|
-
*
|
|
11
|
-
* Generated fields:
|
|
12
|
-
* - bomFormat / specVersion CycloneDX 1.6
|
|
13
|
-
* - serialNumber urn:uuid v4 derived from a stable
|
|
14
|
-
* hash of (project name + version +
|
|
15
|
-
* bundle digest), so identical content
|
|
16
|
-
* reproduces the identical UUID and a
|
|
17
|
-
* rerun that changed nothing is a no-op
|
|
18
|
-
* rather than a spurious diff.
|
|
19
|
-
* - metadata.timestamp the release date this version's
|
|
20
|
-
* CHANGELOG heading declares — NOT the
|
|
21
|
-
* wall-clock moment of generation, which
|
|
22
|
-
* would make the artifact irreproducible
|
|
23
|
-
* - metadata.tools this script itself, version pulled
|
|
24
|
-
* from package.json at refresh time
|
|
25
|
-
* - metadata.component application entry for exceptd-skills,
|
|
26
|
-
* including a hashes[] bundle digest
|
|
27
|
-
* that operators can recompute from
|
|
28
|
-
* the per-file component list (see
|
|
29
|
-
* `bundleDigest` below for the exact
|
|
30
|
-
* canonical-input rule)
|
|
31
|
-
* - metadata.properties catalog count, skill count, dataflow
|
|
32
|
-
* inputs, and the per-skill Ed25519
|
|
33
|
-
* integrity claim (lib/sign.js)
|
|
34
|
-
* - components vendored libraries + a `type: file`
|
|
35
|
-
* component per shipped file in the
|
|
36
|
-
* package.json `files` allowlist, each
|
|
37
|
-
* carrying its SHA-256 hash. Lets
|
|
38
|
-
* CycloneDX-aware vuln scanners verify
|
|
39
|
-
* individual files against the bundle
|
|
40
|
-
* without re-deriving the canonical
|
|
41
|
-
* list themselves.
|
|
42
|
-
* - dependencies [] — nothing to depend on
|
|
43
|
-
*
|
|
44
|
-
* Run: node scripts/refresh-sbom.js
|
|
45
|
-
* npm run refresh-sbom
|
|
46
|
-
*
|
|
47
|
-
* No external dependencies. Node 24 stdlib only.
|
|
3
|
+
* Regenerates sbom.cdx.json, a CycloneDX 1.6 bundle. Every generated field is
|
|
4
|
+
* deterministic — identical content reproduces an identical SBOM, which is what
|
|
5
|
+
* makes the currency gate's regenerate-and-compare mean anything.
|
|
48
6
|
*/
|
|
49
7
|
|
|
50
8
|
'use strict';
|
|
@@ -71,9 +29,7 @@ function listDataCatalogs(dir) {
|
|
|
71
29
|
.sort();
|
|
72
30
|
}
|
|
73
31
|
|
|
74
|
-
/* RFC 4122 v4 UUID derived
|
|
75
|
-
* given (project, version, timestamp) triple maps to a stable UUID
|
|
76
|
-
* across observers. Uses crypto.randomUUID() fallback if no seed. */
|
|
32
|
+
/* RFC 4122 v4 UUID derived from a seed, so one seed maps to one UUID. */
|
|
77
33
|
function uuidV4FromSeed(seed) {
|
|
78
34
|
const hash = crypto.createHash('sha256').update(seed).digest();
|
|
79
35
|
const b = Buffer.from(hash.subarray(0, 16));
|
|
@@ -103,19 +59,7 @@ function loadVendorProvenance() {
|
|
|
103
59
|
}
|
|
104
60
|
}
|
|
105
61
|
|
|
106
|
-
/*
|
|
107
|
-
* concrete file list that npm pack would ship. The allowlist accepts
|
|
108
|
-
* either a file path or a directory path (with trailing slash convention
|
|
109
|
-
* inside this repo); directories expand to every regular file beneath
|
|
110
|
-
* them. Returned paths are POSIX-style relative to REPO_ROOT so the
|
|
111
|
-
* SHA-256 input is stable across operating systems.
|
|
112
|
-
*
|
|
113
|
-
* Mirrors npm's pack-time inclusion rules at the level of fidelity this
|
|
114
|
-
* SBOM needs (a deeper match — .npmignore, package-lock fields, npm-CLI
|
|
115
|
-
* defaults — is intentionally out of scope: any divergence here surfaces
|
|
116
|
-
* as a SHA mismatch on the predeploy verify-shipped-tarball gate, which
|
|
117
|
-
* is the authoritative consumer-side check).
|
|
118
|
-
*/
|
|
62
|
+
/* Every regular file beneath absDir, in name order for a stable inventory. */
|
|
119
63
|
function walkFiles(absDir) {
|
|
120
64
|
const out = [];
|
|
121
65
|
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
@@ -130,42 +74,18 @@ function walkFiles(absDir) {
|
|
|
130
74
|
return out;
|
|
131
75
|
}
|
|
132
76
|
|
|
133
|
-
/*
|
|
134
|
-
* `sbom.cdx.json` is the obvious self-reference: hashing it would always
|
|
135
|
-
* be stale the moment the SBOM gets written back. The bundle digest in
|
|
136
|
-
* metadata.component.hashes[] covers everything ELSE that ships and is
|
|
137
|
-
* the operator's verification anchor for the bundle as a whole. */
|
|
77
|
+
/* No stable SHA inside the SBOM that contains it: the hash is stale on write. */
|
|
138
78
|
const SELF_EXCLUDED = new Set(['sbom.cdx.json']);
|
|
139
79
|
|
|
140
|
-
/*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* deliberately mutates these files (build-incremental.test.js,
|
|
144
|
-
* indexes-v070.test.js), so per-file SHA verification would race against
|
|
145
|
-
* any test run that touches the cache between refresh-sbom and the
|
|
146
|
-
* verification gate. The bundle digest at metadata.component.hashes[] is
|
|
147
|
-
* computed from a SBOM-generation-time snapshot of all OTHER files; the
|
|
148
|
-
* cache is excluded from the per-file inventory entirely. Predeploy's
|
|
149
|
-
* `Pre-computed indexes freshness` gate is the authoritative consumer-
|
|
150
|
-
* side check for the cache. */
|
|
80
|
+
/* Derivable cache artifacts, kept out of the per-file inventory: the test suite
|
|
81
|
+
* mutates data/_indexes/, so a pinned per-file hash would race any run that
|
|
82
|
+
* touches the cache. Predeploy's index-freshness gate covers them instead. */
|
|
151
83
|
const DERIVABLE_PREFIXES = ['data/_indexes/'];
|
|
152
84
|
|
|
153
|
-
/* Files npm
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* inventory — the SBOM described 247 of the 268 files an operator receives and
|
|
158
|
-
* said nothing about the gap. It is not derivable and not self-referential, so
|
|
159
|
-
* nothing but the omission kept it out. README.md and LICENSE get the same
|
|
160
|
-
* unconditional treatment from npm but are already named in `files`, so a
|
|
161
|
-
* union covers the general rule without double-counting them. sources/README.md
|
|
162
|
-
* is here for the same reason: npm collects README files it finds, `files` never
|
|
163
|
-
* names that directory, and it shipped unhashed.
|
|
164
|
-
*
|
|
165
|
-
* This list is maintained by hand, which is only safe because something else
|
|
166
|
-
* checks it: lib/validate-package.js compares the real `npm pack` output against
|
|
167
|
-
* this SBOM, so a file npm decides to ship that is missing here fails a gate
|
|
168
|
-
* instead of shipping silently. */
|
|
85
|
+
/* Files npm ships whatever the `files` allowlist says, unioned in so they are
|
|
86
|
+
* hashed too. The list is hand-maintained, which is only safe because
|
|
87
|
+
* lib/validate-package.js compares the real `npm pack` output against this SBOM
|
|
88
|
+
* — a shipped file missing here fails a gate. */
|
|
169
89
|
const ALWAYS_SHIPPED = ['package.json', 'sources/README.md'];
|
|
170
90
|
|
|
171
91
|
function isDerivable(rel) {
|
|
@@ -184,9 +104,9 @@ function expandAllowlist(allowlist) {
|
|
|
184
104
|
abs.push(full);
|
|
185
105
|
}
|
|
186
106
|
}
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
107
|
+
// Deduped and sorted by POSIX-relative path, so the SHA-256 input is the same
|
|
108
|
+
// on every operating system. npm's pack rules are matched only as far as this
|
|
109
|
+
// SBOM needs; verify-shipped-tarball is the authoritative check.
|
|
190
110
|
const rel = Array.from(new Set(abs.map((a) => toPosixRel(a))))
|
|
191
111
|
.filter((r) => !SELF_EXCLUDED.has(r))
|
|
192
112
|
.filter((r) => !isDerivable(r))
|
|
@@ -195,22 +115,10 @@ function expandAllowlist(allowlist) {
|
|
|
195
115
|
}
|
|
196
116
|
|
|
197
117
|
/* metadata.timestamp — the release date this version's CHANGELOG heading
|
|
198
|
-
* declares, as an ISO-8601 instant.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* would differ on every run and the comparison could never mean anything.
|
|
203
|
-
* The previous approach kept determinism by folding the bundle hash into a
|
|
204
|
-
* date offset, but the offset was a full uint32 of SECONDS — a 136-year
|
|
205
|
-
* spread — so the field routinely landed a century or more in the future
|
|
206
|
-
* (the shipped value read 2147-11-20). Deterministic, and untrue: consumers
|
|
207
|
-
* that sort or age-check SBOMs read that as a real creation date.
|
|
208
|
-
*
|
|
209
|
-
* The CHANGELOG heading is both deterministic and true. Its format is already
|
|
210
|
-
* enforced by scripts/check-changelog-extract.js, which the release flow runs
|
|
211
|
-
* before this script, so the date is guaranteed present by the time a release
|
|
212
|
-
* regenerates the SBOM. Refusing is deliberate when it is absent — inventing a
|
|
213
|
-
* placeholder is what produced the 2147 date in the first place. */
|
|
118
|
+
* declares, as an ISO-8601 instant. It must be deterministic for the currency
|
|
119
|
+
* gate's regenerate-and-compare, and true because consumers age-check on it, so
|
|
120
|
+
* a missing heading throws rather than synthesizing a value.
|
|
121
|
+
* scripts/check-changelog-extract.js enforces the heading format. */
|
|
214
122
|
function releaseTimestamp(version) {
|
|
215
123
|
const changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
|
|
216
124
|
const text = fs.readFileSync(changelogPath, 'utf8');
|
|
@@ -240,22 +148,9 @@ function sha256File(absPath) {
|
|
|
240
148
|
.digest('hex');
|
|
241
149
|
}
|
|
242
150
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// - SHA-256 stays as the universal-tool contract (Anchore / Trivy /
|
|
248
|
-
// Dependency-Track / GitHub Dependency Graph all parse it).
|
|
249
|
-
// - SHA3-512 is the SHA-3 family (Keccak / sponge), different
|
|
250
|
-
// mathematical foundation. Hedges against future SHA-2 weaknesses
|
|
251
|
-
// and aligns with the project's PQ posture (ML-KEM / ML-DSA both
|
|
252
|
-
// internally hash with SHA-3).
|
|
253
|
-
//
|
|
254
|
-
// check-sbom-currency.js verifies BOTH when present and refuses if a
|
|
255
|
-
// SHA3-512 entry is recorded but its content drifts from the live
|
|
256
|
-
// bytes — so a downgrade attack that drops SHA3-512 from the recorded
|
|
257
|
-
// SBOM (leaving only SHA-256) is observable as a missing-hash error,
|
|
258
|
-
// not a silent acceptance.
|
|
151
|
+
// SHA3-512 sits alongside SHA-256 on every file component: a different
|
|
152
|
+
// construction to fall back on. check-sbom-currency.js verifies both, so
|
|
153
|
+
// dropping the SHA3-512 entry reads as a missing-hash error, not acceptance.
|
|
259
154
|
function sha3_512File(absPath) {
|
|
260
155
|
return crypto
|
|
261
156
|
.createHash('sha3-512')
|
|
@@ -281,12 +176,9 @@ function fileComponents(allowlist) {
|
|
|
281
176
|
return out;
|
|
282
177
|
}
|
|
283
178
|
|
|
284
|
-
/*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* components[] list (`type: file` entries) lets them recompute and
|
|
288
|
-
* compare without trusting the SBOM's stored value blindly.
|
|
289
|
-
*/
|
|
179
|
+
/* SHA-256 over a "<sha256>\t<relpath>\n" stream of every shipped file, sorted by
|
|
180
|
+
* relpath — the same input an operator assembles from the `type: file` entries
|
|
181
|
+
* in components[], so the stored digest can be recomputed rather than trusted. */
|
|
290
182
|
function bundleDigest(fileComps) {
|
|
291
183
|
const sorted = [...fileComps].sort((a, b) =>
|
|
292
184
|
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
@@ -337,25 +229,12 @@ function buildSbom() {
|
|
|
337
229
|
const fileComps = fileComponents(Array.isArray(pkg.files) ? pkg.files : []);
|
|
338
230
|
const bundleSha = bundleDigest(fileComps);
|
|
339
231
|
|
|
340
|
-
//
|
|
341
|
-
// deterministic regeneration.
|
|
232
|
+
// Sorted by bom-ref so regeneration is byte-identical.
|
|
342
233
|
const allComponents = [...vendoredComponents, ...fileComps].sort((a, b) =>
|
|
343
234
|
a['bom-ref'] < b['bom-ref'] ? -1 : a['bom-ref'] > b['bom-ref'] ? 1 : 0,
|
|
344
235
|
);
|
|
345
236
|
|
|
346
|
-
//
|
|
347
|
-
// bundle content hash, not wall-clock. Pre-v0.13 every refresh produced
|
|
348
|
-
// a new UUID + timestamp even when the bundle content was byte-identical,
|
|
349
|
-
// so the SBOM-currency gate produced noisy diffs and the predeploy
|
|
350
|
-
// comparison could not rely on stable byte-identity. The comment at the
|
|
351
|
-
// top of this file says "stable across observers" — the implementation
|
|
352
|
-
// contradicted it. Now: identical content → identical SBOM.
|
|
353
|
-
//
|
|
354
|
-
// The synthetic timestamp uses the bundle SHA folded into a date string
|
|
355
|
-
// anchored at the Unix epoch + a deterministic offset; this is NOT a
|
|
356
|
-
// real audit timestamp (the `metadata.lifecycles[]` block carries the
|
|
357
|
-
// intended-lifecycle phase for that). Operators wanting the wall-clock
|
|
358
|
-
// time of a refresh should read the file's mtime or refresh-report.json.
|
|
237
|
+
// Seeded from the bundle content, not the clock: a no-op refresh is a no-op.
|
|
359
238
|
const seed = `${pkg.name}@${pkg.version}@${bundleSha}`;
|
|
360
239
|
const serialNumber = 'urn:uuid:' + uuidV4FromSeed(seed);
|
|
361
240
|
const timestamp = releaseTimestamp(pkg.version);
|
|
@@ -379,9 +258,6 @@ function buildSbom() {
|
|
|
379
258
|
},
|
|
380
259
|
],
|
|
381
260
|
component: {
|
|
382
|
-
// Switch from project: scheme to pkg:npm scheme post-v0.9.0 — the
|
|
383
|
-
// package is now published on npm with provenance attestation, so
|
|
384
|
-
// the CycloneDX bom-ref should reflect the canonical PURL.
|
|
385
261
|
'bom-ref': `pkg:npm/${pkg.name}@${pkg.version}`,
|
|
386
262
|
type: 'application',
|
|
387
263
|
name: pkg.name,
|
|
@@ -389,10 +265,7 @@ function buildSbom() {
|
|
|
389
265
|
description: pkg.description,
|
|
390
266
|
licenses: [{ license: { id: 'Apache-2.0' } }],
|
|
391
267
|
purl: `pkg:npm/${pkg.name.replace(/@/g, '%40')}@${pkg.version}`,
|
|
392
|
-
//
|
|
393
|
-
// for the canonical-input rule). Operators can recompute this
|
|
394
|
-
// from the per-file components[] list and compare without
|
|
395
|
-
// re-deriving package.json.files themselves.
|
|
268
|
+
// Recomputable from components[]; bundleDigest above states the input rule.
|
|
396
269
|
hashes: [{ alg: 'SHA-256', content: bundleSha }],
|
|
397
270
|
externalReferences: [
|
|
398
271
|
{ type: 'distribution', url: `https://www.npmjs.com/package/${pkg.name}/v/${pkg.version}` },
|
|
@@ -416,11 +289,8 @@ function buildSbom() {
|
|
|
416
289
|
name: 'exceptd:integrity:method',
|
|
417
290
|
value: 'Ed25519 per-skill (lib/sign.js)',
|
|
418
291
|
},
|
|
419
|
-
// An operator verifying the bundle holds the SBOM, not this script
|
|
420
|
-
//
|
|
421
|
-
// deliberately omits was a source comment they never see, so a partial
|
|
422
|
-
// inventory was indistinguishable from a complete one. State the
|
|
423
|
-
// uncovered prefix and the check that covers it instead.
|
|
292
|
+
// An operator verifying the bundle holds the SBOM, not this script: these
|
|
293
|
+
// two properties say the inventory is partial by design, and what covers it.
|
|
424
294
|
{
|
|
425
295
|
name: 'exceptd:integrity:uncovered:prefix',
|
|
426
296
|
value: DERIVABLE_PREFIXES.join(','),
|