@blamejs/exceptd-skills 0.19.33 → 0.19.35
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/bin/exceptd.js +895 -2828
- package/data/_indexes/_meta.json +2 -2
- 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 +170 -76
- package/lib/collectors/cicd-pipeline-compromise.js +113 -136
- 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 +198 -211
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +130 -118
- package/lib/collectors/scan-excludes.js +33 -139
- 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 -155
- 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 +39 -113
- 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 +88 -236
- package/lib/playbook-runner.js +759 -2107
- package/lib/prefetch.js +101 -376
- package/lib/refresh-external.js +199 -633
- package/lib/refresh-network.js +78 -311
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +85 -146
- 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 +28 -27
- package/lib/upstream-check-cli.js +36 -29
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +52 -121
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +78 -286
- 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 -413
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +242 -242
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +29 -28
- 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 +21 -31
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +26 -57
- 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 +63 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +62 -81
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +83 -198
- 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 +7 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +7 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +7 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +63 -148
- package/scripts/release.js +69 -234
- package/scripts/run-e2e-scenarios.js +26 -73
- 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 -141
|
@@ -2,41 +2,11 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Packs with `npm pack`, extracts the tarball, and verifies the EXTRACTED tree
|
|
6
|
+
* rather than the source working tree: a keys/public.pem swapped between sign
|
|
7
|
+
* and pack surfaces nowhere else, and every fresh install then fails.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
* then run lib/verify.js against the EXTRACTED tree (not the source
|
|
9
|
-
* working tree). This catches the class of bug where:
|
|
10
|
-
*
|
|
11
|
-
* - CI's verify step against the source tree passes (38/38)
|
|
12
|
-
* - The tarball that npm publish actually uploads has different
|
|
13
|
-
* content (e.g. keys/public.pem swapped) and verify-on-tarball fails
|
|
14
|
-
*
|
|
15
|
-
* Every release v0.11.x through v0.12.2 shipped a tarball whose
|
|
16
|
-
* keys/public.pem did not match the Ed25519 signatures in manifest.json.
|
|
17
|
-
* Operators installing from npm saw 0/38 verify on every fresh install.
|
|
18
|
-
* The bug was invisible because CI's verify ran against the SOURCE tree,
|
|
19
|
-
* not the shipped tarball. This gate closes that gap.
|
|
20
|
-
*
|
|
21
|
-
* * F9 — After the first-pass extraction (using the source-tree parseTar),
|
|
22
|
-
* re-parse the tarball using the parseTar shipped INSIDE the
|
|
23
|
-
* extracted tree itself. If the two parses disagree, fail with a
|
|
24
|
-
* structured error. Catches the class where the shipped parser
|
|
25
|
-
* silently rejects entries the source parser accepts (or vice
|
|
26
|
-
* versa), which would mean operators run a different extractor
|
|
27
|
-
* than CI exercised.
|
|
28
|
-
* F15 — Invoke `npm pack --offline` so the gate cannot be blocked by
|
|
29
|
-
* registry reachability problems during predeploy.
|
|
30
|
-
* F4 — Cross-check the extracted public.pem against
|
|
31
|
-
* keys/EXPECTED_FINGERPRINT (warn-and-continue when missing, fail
|
|
32
|
-
* when present-but-mismatched and KEYS_ROTATED != 1).
|
|
33
|
-
*
|
|
34
|
-
* Exit codes:
|
|
35
|
-
* 0 verify passed against the packed tarball
|
|
36
|
-
* 1 verify failed against the packed tarball (the bug class above)
|
|
37
|
-
* 2 pack or extract failed (infrastructure error)
|
|
38
|
-
*
|
|
39
|
-
* Zero npm deps. Node 24 stdlib only.
|
|
9
|
+
* Exit: 0 verified, 1 verify failed, 2 pack or extract failed.
|
|
40
10
|
*/
|
|
41
11
|
|
|
42
12
|
const fs = require("fs");
|
|
@@ -44,26 +14,17 @@ const path = require("path");
|
|
|
44
14
|
const os = require("os");
|
|
45
15
|
const { spawnSync } = require("child_process");
|
|
46
16
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// a bug in the normalize() implementation in lib/ doesn't simultaneously
|
|
51
|
-
// disable both the source-tree-verify path AND the shipped-tarball-verify
|
|
52
|
-
// gate (we want at least one independent check). ANY change to normalize()
|
|
53
|
-
// in any of these four files must be mirrored in all of them.
|
|
17
|
+
// Mirrors the normalize() contract in lib/sign.js, lib/verify.js and
|
|
18
|
+
// lib/refresh-network.js. Duplicated deliberately: one bug must not disable the
|
|
19
|
+
// source-tree and shipped-tarball checks together. Change all four together.
|
|
54
20
|
function normalizeSkillBytes(buf) {
|
|
55
21
|
let s = Buffer.isBuffer(buf) ? buf.toString("utf8") : String(buf);
|
|
56
22
|
if (s.length > 0 && s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
57
23
|
return Buffer.from(s.replace(/\r\n/g, "\n"), "utf8");
|
|
58
24
|
}
|
|
59
25
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// normalizeSkillBytes: a bug in lib/verify.js's verifier should not also
|
|
63
|
-
// disable this gate — at least one independent check must remain. The
|
|
64
|
-
// canonical-bytes computation MUST stay in lockstep with lib/sign.js +
|
|
65
|
-
// lib/verify.js + lib/refresh-network.js — enforced by
|
|
66
|
-
// tests/normalize-contract.test.js.
|
|
26
|
+
// Duplicated for the same reason as normalizeSkillBytes; the canonical-bytes
|
|
27
|
+
// computation stays in lockstep, enforced by tests/normalize-contract.test.js.
|
|
67
28
|
function canonicalizeForTarball(value) {
|
|
68
29
|
if (Array.isArray(value)) return value.map(canonicalizeForTarball);
|
|
69
30
|
if (value && typeof value === "object") {
|
|
@@ -78,7 +39,6 @@ function canonicalizeForTarball(value) {
|
|
|
78
39
|
function canonicalManifestBytesForTarball(manifest) {
|
|
79
40
|
const clone = Object.assign({}, manifest);
|
|
80
41
|
delete clone.manifest_signature;
|
|
81
|
-
const cryptoMod = require("crypto"); // eslint-disable-line no-unused-vars
|
|
82
42
|
const json = JSON.stringify(canonicalizeForTarball(clone), null, 2);
|
|
83
43
|
return normalizeSkillBytes(Buffer.from(json, "utf8"));
|
|
84
44
|
}
|
|
@@ -108,20 +68,9 @@ function verifyExtractedManifestSignature(manifest, publicKeyPem) {
|
|
|
108
68
|
return ok ? { status: "valid" } : { status: "invalid", reason: "Ed25519 manifest signature did not verify against extracted public.pem" };
|
|
109
69
|
}
|
|
110
70
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// branch: a manifest skill entry MAY legally omit `signature` (it is not
|
|
115
|
-
// in the schema's skillEntry.required set — a freshly-added skill, or one
|
|
116
|
-
// skipped during a partial sign-all, leaves it absent while the manifest
|
|
117
|
-
// envelope is still validly re-signed over that field-absent state). Such
|
|
118
|
-
// an entry must be a STRUCTURED miss, not Buffer.from(undefined,'base64')
|
|
119
|
-
// throwing a TypeError that the script body's catch re-throws as an
|
|
120
|
-
// uncaught stack trace.
|
|
121
|
-
// { status: 'missing' } — no Ed25519 signature in manifest
|
|
122
|
-
// { status: 'pass' } — signature verified
|
|
123
|
-
// { status: 'fail' } — signature did not verify
|
|
124
|
-
// { status: 'fail', reason: str } — malformed base64 / verify threw
|
|
71
|
+
// One skill entry's outcome: { status: 'missing' | 'pass' | 'fail' }, plus a
|
|
72
|
+
// `reason` when the base64 is malformed or verify throws. A manifest entry may
|
|
73
|
+
// legally omit `signature`, so that case is a structured miss, not a throw.
|
|
125
74
|
function verifySkillSignatureOutcome(s, normalizedContent, pubKey) {
|
|
126
75
|
const cryptoMod = require("crypto");
|
|
127
76
|
if (typeof s.signature !== "string" || s.signature.length === 0) {
|
|
@@ -142,9 +91,6 @@ function verifySkillSignatureOutcome(s, normalizedContent, pubKey) {
|
|
|
142
91
|
return ok ? { status: "pass" } : { status: "fail" };
|
|
143
92
|
}
|
|
144
93
|
|
|
145
|
-
// Exported so tests/normalize-contract.test.js can assert byte-identical
|
|
146
|
-
// normalize() behavior across all four implementations, and so the
|
|
147
|
-
// missing-signature guard above is unit-testable.
|
|
148
94
|
module.exports = {
|
|
149
95
|
normalizeSkillBytes,
|
|
150
96
|
verifyExtractedManifestSignature,
|
|
@@ -155,10 +101,8 @@ module.exports = {
|
|
|
155
101
|
const ROOT = path.resolve(__dirname, "..");
|
|
156
102
|
|
|
157
103
|
function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
|
|
158
|
-
// Sentinel thrown by fail() so the
|
|
159
|
-
//
|
|
160
|
-
// npm-pack temp dir on every run (predeploy gate + `npm test`). Abort by
|
|
161
|
-
// throwing instead and set the exit code via process.exitCode.
|
|
104
|
+
// Sentinel thrown by fail() so the body's finally still runs its temp-dir
|
|
105
|
+
// cleanup — process.exit() would preempt it and leak the npm-pack temp dir.
|
|
162
106
|
const ABORT = Symbol("verify-shipped-tarball:abort");
|
|
163
107
|
function fail(msg, code = 1) {
|
|
164
108
|
process.stderr.write(`[verify-shipped-tarball] FAIL: ${msg}\n`);
|
|
@@ -166,22 +110,15 @@ function fail(msg, code = 1) {
|
|
|
166
110
|
throw ABORT;
|
|
167
111
|
}
|
|
168
112
|
|
|
169
|
-
//
|
|
170
|
-
// `require()` this file to load the exported helpers (notably
|
|
171
|
-
// normalizeSkillBytes for the byte-stability contract test) without
|
|
172
|
-
// invoking npm pack as a side effect of import.
|
|
113
|
+
// Gated on require.main so a test can require() the helpers without npm pack running.
|
|
173
114
|
if (require.main !== module) {
|
|
174
|
-
// Loaded as a library (e.g. by tests/normalize-contract.test.js).
|
|
175
|
-
// Skip the script body; consumers use the module.exports surface above.
|
|
176
115
|
return;
|
|
177
116
|
}
|
|
178
117
|
|
|
179
118
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "verify-shipped-"));
|
|
180
119
|
try {
|
|
181
120
|
emit(`packing into ${tmpRoot} ...`);
|
|
182
|
-
//
|
|
183
|
-
// reachability; `npm pack` does not need the network for a local
|
|
184
|
-
// package and forcing offline mode hard-locks the assumption.
|
|
121
|
+
// --offline: predeploy must run without registry reachability.
|
|
185
122
|
const pack = spawnSync("npm", ["pack", "--offline", "--pack-destination", tmpRoot], {
|
|
186
123
|
cwd: ROOT,
|
|
187
124
|
encoding: "utf8",
|
|
@@ -192,9 +129,7 @@ try {
|
|
|
192
129
|
}
|
|
193
130
|
const tarballName = pack.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
|
|
194
131
|
const tarballPath = path.join(tmpRoot, tarballName);
|
|
195
|
-
//
|
|
196
|
-
// existence check, size report, and read all observe the same inode — no
|
|
197
|
-
// existsSync→statSync→readFileSync TOCTOU on the path.
|
|
132
|
+
// One descriptor for fstat + read, so both observe the same inode — no TOCTOU.
|
|
198
133
|
let tgz;
|
|
199
134
|
try {
|
|
200
135
|
const fd = fs.openSync(tarballPath, "r");
|
|
@@ -229,12 +164,8 @@ try {
|
|
|
229
164
|
}
|
|
230
165
|
emit(`extracted to ${pkgRoot}`);
|
|
231
166
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// gate trips: this means CI exercised a different parser than operators
|
|
235
|
-
// will. Defense against drift between source and shipped tarball when
|
|
236
|
-
// someone edits lib/refresh-network.js without re-vendoring or vice
|
|
237
|
-
// versa.
|
|
167
|
+
// Re-parse with the extracted tree's OWN parseTar: a divergence means CI
|
|
168
|
+
// exercised a different extractor than operators will run.
|
|
238
169
|
const shippedParserPath = path.join(pkgRoot, "lib", "refresh-network.js");
|
|
239
170
|
if (!fs.existsSync(shippedParserPath)) {
|
|
240
171
|
fail(`extracted tree missing lib/refresh-network.js (cannot run F9 cross-parse check)`, 2);
|
|
@@ -249,7 +180,6 @@ try {
|
|
|
249
180
|
fail(`extracted lib/refresh-network.js does not export parseTar`, 2);
|
|
250
181
|
}
|
|
251
182
|
const shippedEntries = parseTarShipped(tarBuf);
|
|
252
|
-
// Compare counts first — fast bailout.
|
|
253
183
|
const divergences = [];
|
|
254
184
|
if (shippedEntries.length !== entries.length) {
|
|
255
185
|
divergences.push(
|
|
@@ -257,8 +187,7 @@ try {
|
|
|
257
187
|
`shipped parser produced ${shippedEntries.length}`
|
|
258
188
|
);
|
|
259
189
|
} else {
|
|
260
|
-
//
|
|
261
|
-
// compare is correct. Compare name + byte length + body bytes.
|
|
190
|
+
// Tarball entry order is deterministic, so a positional compare is valid.
|
|
262
191
|
for (let i = 0; i < entries.length; i++) {
|
|
263
192
|
const a = entries[i];
|
|
264
193
|
const b = shippedEntries[i];
|
|
@@ -288,9 +217,7 @@ try {
|
|
|
288
217
|
}
|
|
289
218
|
emit(`F9: source-tree and shipped parseTar agree on ${entries.length} entries`);
|
|
290
219
|
|
|
291
|
-
//
|
|
292
|
-
// having to spawn a separate process whose cwd resolution differs across
|
|
293
|
-
// platforms.
|
|
220
|
+
// Inline rather than spawned: cwd resolution differs across platforms.
|
|
294
221
|
const crypto = require("crypto");
|
|
295
222
|
const manifestPath = path.join(pkgRoot, "manifest.json");
|
|
296
223
|
const pubKeyPath = path.join(pkgRoot, "keys", "public.pem");
|
|
@@ -301,16 +228,9 @@ try {
|
|
|
301
228
|
const pubPem = fs.readFileSync(pubKeyPath, "utf8");
|
|
302
229
|
const pubKey = crypto.createPublicKey(pubPem);
|
|
303
230
|
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// other manifest envelope metadata. A tarball whose body bytes are
|
|
308
|
-
// signed but whose manifest envelope was rewritten (re-routing a skill
|
|
309
|
-
// path, renaming a skill, changing atlas refs) would pass per-skill
|
|
310
|
-
// verification but fail this gate. v0.12.17+ shipped tarballs always
|
|
311
|
-
// include manifest_signature, so a missing signature here is also a
|
|
312
|
-
// refusal — stricter than the post-install warn-and-continue path,
|
|
313
|
-
// which tolerates legacy v0.12.16-and-earlier installs.
|
|
231
|
+
// Per-skill signatures cover only the skill body bytes, not skill.name or
|
|
232
|
+
// skill.path — a rewritten manifest envelope still passes per-skill verify.
|
|
233
|
+
// A missing envelope signature is a refusal here, not a warning.
|
|
314
234
|
const manifestSigStatus = verifyExtractedManifestSignature(manifest, pubPem);
|
|
315
235
|
if (manifestSigStatus.status !== "valid") {
|
|
316
236
|
fail(
|
|
@@ -324,8 +244,7 @@ try {
|
|
|
324
244
|
.update(pubKey.export({ type: "spki", format: "der" }))
|
|
325
245
|
.digest("base64");
|
|
326
246
|
|
|
327
|
-
//
|
|
328
|
-
// shows the divergence explicitly.
|
|
247
|
+
// The same fingerprint for the SOURCE-tree key, so the log names the drift.
|
|
329
248
|
const sourcePubPem = fs.readFileSync(path.join(ROOT, "keys", "public.pem"), "utf8");
|
|
330
249
|
const sourcePubKey = crypto.createPublicKey(sourcePubPem);
|
|
331
250
|
const sourcePubFp = crypto.createHash("sha256")
|
|
@@ -339,25 +258,17 @@ try {
|
|
|
339
258
|
emit(`*** Something between sign and pack is swapping the key. Verify will fail below. ***`);
|
|
340
259
|
}
|
|
341
260
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
// that's the file operators will actually receive on `npm install`.
|
|
345
|
-
// Warn when absent, fail when present-but-mismatched (unless KEYS_ROTATED).
|
|
261
|
+
// The pin is read from the EXTRACTED tree — the file operators receive on
|
|
262
|
+
// install. Absent warns; mismatched fails unless KEYS_ROTATED=1.
|
|
346
263
|
const expectedFpPath = path.join(pkgRoot, "keys", "EXPECTED_FINGERPRINT");
|
|
347
264
|
if (fs.existsSync(expectedFpPath)) {
|
|
348
|
-
//
|
|
349
|
-
// file (Notepad with files.encoding=utf8bom in the source tree) is
|
|
350
|
-
// tolerated identically across every verify site. The helper strips
|
|
351
|
-
// leading U+FEFF + ignores comment lines (`#`).
|
|
265
|
+
// The shared loader strips a leading U+FEFF, so a BOM-prefixed pin is tolerated.
|
|
352
266
|
const { loadExpectedFingerprintFirstLine } = require(path.join(ROOT, "lib", "verify.js"));
|
|
353
267
|
const firstLine = loadExpectedFingerprintFirstLine(expectedFpPath) || "";
|
|
354
268
|
const liveFpLine = `SHA256:${pubFp}`;
|
|
355
269
|
if (firstLine !== liveFpLine) {
|
|
356
270
|
if (process.env.KEYS_ROTATED === "1") {
|
|
357
|
-
//
|
|
358
|
-
// well so any caller listening on `process.on('warning')` can
|
|
359
|
-
// observe the key-rotation acceptance. Matches the three peer
|
|
360
|
-
// sites (bin/exceptd.js, lib/refresh-network.js, lib/verify.js).
|
|
271
|
+
// Also on the structured warning channel, so a listener observes it.
|
|
361
272
|
process.emitWarning(
|
|
362
273
|
`extracted public.pem fingerprint ${liveFpLine} differs from pin ${firstLine}; KEYS_ROTATED=1 accepted. ` +
|
|
363
274
|
`Update keys/EXPECTED_FINGERPRINT to lock the new pin.`,
|
|
@@ -389,23 +300,12 @@ try {
|
|
|
389
300
|
failures.push(`${s.name}: file not found at ${s.path}`);
|
|
390
301
|
continue;
|
|
391
302
|
}
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// the byte-stability contract in lib/verify.js's normalize() header.
|
|
396
|
-
// Without the same normalization here, this gate (which was added
|
|
397
|
-
// specifically to catch the v0.11.x signature regression class!) would
|
|
398
|
-
// itself report 0/38 on any tree where line-ending normalization
|
|
399
|
-
// touched the source between sign and pack — a Windows contributor
|
|
400
|
-
// with `core.autocrlf=true`, or a tool like Prettier between sign and
|
|
401
|
-
// pack. This is the recurring CRLF/line-ending-bypass class.
|
|
303
|
+
// Normalize before verify: lib/sign.js signs BOM-stripped, LF-normalized
|
|
304
|
+
// bytes, so feeding raw bytes here reports 0/N wherever line endings changed
|
|
305
|
+
// between sign and pack (`core.autocrlf=true`).
|
|
402
306
|
const rawContent = fs.readFileSync(skillPath);
|
|
403
307
|
const normalizedContent = normalizeSkillBytes(rawContent);
|
|
404
308
|
const outcome = verifySkillSignatureOutcome(s, normalizedContent, pubKey);
|
|
405
|
-
// A skill entry that legally omits `signature` (not in the schema's
|
|
406
|
-
// required set) must be a STRUCTURED miss — not Buffer.from(undefined,
|
|
407
|
-
// 'base64') throwing a TypeError that the body's catch re-throws as an
|
|
408
|
-
// uncaught stack trace. Mirrors lib/verify.js verifySkill()'s missing_sig.
|
|
409
309
|
if (outcome.status === "missing") {
|
|
410
310
|
miss++;
|
|
411
311
|
failures.push(`${s.name}: no Ed25519 signature in manifest`);
|
|
@@ -415,13 +315,8 @@ try {
|
|
|
415
315
|
if (ok) pass++;
|
|
416
316
|
else {
|
|
417
317
|
fail_count++;
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// v0.12.16: forensic logging uses rawContent (pre-normalization
|
|
421
|
-
// bytes) so an operator inspecting failures sees the actual on-disk
|
|
422
|
-
// shape, but tarSha is computed over the NORMALIZED bytes that
|
|
423
|
-
// were actually fed to crypto.verify — making the comparison to
|
|
424
|
-
// sign-time bytes meaningful.
|
|
318
|
+
// Sizes come from rawContent so an operator sees the on-disk shape, but
|
|
319
|
+
// the hashes cover the NORMALIZED bytes actually fed to crypto.verify.
|
|
425
320
|
const tarSha = crypto.createHash("sha256").update(normalizedContent).digest("hex").slice(0, 16);
|
|
426
321
|
let srcSha = "<missing>", srcSize = 0, srcContent;
|
|
427
322
|
if (fs.existsSync(sourceSkillPath)) {
|
|
@@ -447,8 +342,7 @@ try {
|
|
|
447
342
|
process.exitCode = 1;
|
|
448
343
|
}
|
|
449
344
|
} catch (e) {
|
|
450
|
-
// ABORT is the fail() sentinel
|
|
451
|
-
// other error is unexpected: let finally run, then re-propagate it.
|
|
345
|
+
// ABORT is the fail() sentinel; any other error re-propagates after finally.
|
|
452
346
|
if (e !== ABORT) throw e;
|
|
453
347
|
} finally {
|
|
454
348
|
// Best-effort cleanup; leave on failure for diagnostics.
|