@blamejs/exceptd-skills 0.12.18 → 0.12.20

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.
@@ -194,14 +194,63 @@ function verifyDetached(publicKeyObj, payload, sigB64) {
194
194
  // v0.12.14 (audit F1, F7): CRLF/BOM normalization mirrors lib/verify.js's
195
195
  // normalize(). Duplicated here to keep refresh-network free of cross-module
196
196
  // runtime deps. ANY change here MUST be mirrored in lib/verify.js +
197
- // lib/sign.js — the three normalize() implementations form a byte-stability
198
- // contract.
197
+ // lib/sign.js + scripts/verify-shipped-tarball.js — the four normalize()
198
+ // implementations form a byte-stability contract enforced by
199
+ // tests/normalize-contract.test.js.
199
200
  function normalizeSkillBytes(buf) {
200
201
  let s = Buffer.isBuffer(buf) ? buf.toString("utf8") : String(buf);
201
202
  if (s.length > 0 && s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
202
203
  return Buffer.from(s.replace(/\r\n/g, "\n"), "utf8");
203
204
  }
204
205
 
206
+ // Audit O P1-B + Q P1: in-line manifest-signature verifier. Kept here
207
+ // rather than imported from lib/verify.js so refresh-network.js retains
208
+ // its no-cross-module-dep posture (mirrors the per-skill verify path).
209
+ // ANY change to canonical-bytes computation here MUST stay in lockstep
210
+ // with lib/sign.js canonicalManifestBytes() / lib/verify.js
211
+ // canonicalManifestBytes() — tests/normalize-contract.test.js enforces.
212
+ function canonicalizeForRefresh(value) {
213
+ if (Array.isArray(value)) return value.map(canonicalizeForRefresh);
214
+ if (value && typeof value === "object") {
215
+ const out = {};
216
+ for (const key of Object.keys(value).sort()) {
217
+ out[key] = canonicalizeForRefresh(value[key]);
218
+ }
219
+ return out;
220
+ }
221
+ return value;
222
+ }
223
+ function canonicalManifestBytesForRefresh(manifest) {
224
+ const clone = Object.assign({}, manifest);
225
+ delete clone.manifest_signature;
226
+ const json = JSON.stringify(canonicalizeForRefresh(clone), null, 2);
227
+ return normalizeSkillBytes(Buffer.from(json, "utf8"));
228
+ }
229
+ function verifyTarballManifestSignature(manifest, publicKeyPem) {
230
+ const sig = manifest && manifest.manifest_signature;
231
+ if (!sig || typeof sig !== "object") return { status: "missing" };
232
+ if (typeof sig.signature_base64 !== "string") {
233
+ return { status: "invalid", reason: "manifest_signature.signature_base64 missing or not a string" };
234
+ }
235
+ if (sig.algorithm !== "Ed25519") {
236
+ return { status: "invalid", reason: `manifest_signature.algorithm must be 'Ed25519' (got ${JSON.stringify(sig.algorithm)})` };
237
+ }
238
+ let signatureBytes;
239
+ try { signatureBytes = Buffer.from(sig.signature_base64, "base64"); }
240
+ catch (e) { return { status: "invalid", reason: `malformed base64: ${e.message}` }; }
241
+ const bytes = canonicalManifestBytesForRefresh(manifest);
242
+ let ok = false;
243
+ try {
244
+ ok = crypto.verify(null, bytes, {
245
+ key: publicKeyPem,
246
+ dsaEncoding: "ieee-p1363",
247
+ }, signatureBytes);
248
+ } catch (e) {
249
+ return { status: "invalid", reason: `crypto.verify threw: ${e.message}` };
250
+ }
251
+ return ok ? { status: "valid" } : { status: "invalid", reason: "Ed25519 manifest signature did not verify against local public.pem" };
252
+ }
253
+
205
254
  // Manifest path validation. Mirrors lib/verify.js validateSkillPath().
206
255
  function validateManifestSkillPath(skillPath) {
207
256
  if (typeof skillPath !== "string") throw new Error(`manifest skill.path must be a string, got ${typeof skillPath}`);
@@ -402,6 +451,33 @@ async function main() {
402
451
  try { tarballManifest = JSON.parse(tarballManifestEntry.body.toString("utf8")); }
403
452
  catch (e) { emit({ ok: false, error: `tarball manifest.json parse: ${e.message}` }, opts.json); process.exitCode = 4; return; }
404
453
 
454
+ // Audit O P1-B + Q P1: verify the top-level manifest_signature against
455
+ // the LOCAL public key before honoring any entry in the tarball manifest.
456
+ // The previous flow iterated `manifest.skills[].signature` per-skill but
457
+ // never authenticated the manifest envelope itself — a coordinated
458
+ // attacker who flipped paths/names/atlas_refs on entries already covered
459
+ // by per-skill signatures (which sign only the skill body bytes, not the
460
+ // metadata around them) could re-shape catalog routing without breaking
461
+ // any per-skill signature. The manifest signature closes that gap.
462
+ //
463
+ // Unlike post-install verify (which warns-and-continues on missing
464
+ // signature for legacy-tarball compat), refresh-network REQUIRES the
465
+ // signature: this code path is publishing fresh content into the local
466
+ // tree, and the tarball must already be ≥ v0.12.17 to have reached the
467
+ // registry through the sign-all gate.
468
+ const manifestSigResult = verifyTarballManifestSignature(tarballManifest, localPubKeyText);
469
+ if (manifestSigResult.status !== "valid") {
470
+ emit({
471
+ ok: false,
472
+ error: `tarball manifest_signature ${manifestSigResult.status} — refusing to swap`,
473
+ reason: manifestSigResult.reason || null,
474
+ hint: manifestSigResult.status === "missing"
475
+ ? "Tarball predates v0.12.17 manifest signing. Run `npm update -g @blamejs/exceptd-skills` instead so the full provenance-verified install path runs."
476
+ : "Tarball manifest envelope failed Ed25519 verification against the LOCAL public key. Run `npm update -g @blamejs/exceptd-skills` for the full provenance-verified path, or report this tarball at https://github.com/blamejs/exceptd-skills/issues.",
477
+ }, opts.json);
478
+ process.exitCode = 5; return;
479
+ }
480
+
405
481
  // v0.12.14 (audit F1): the prior loop iterated `sk.id` + a fixed payload
406
482
  // path `skills/<id>/SKILL.md`. Manifest entries actually expose `name` +
407
483
  // `path` (a forward-slash relative path like `skills/<name>/skill.md`,
@@ -603,4 +679,16 @@ if (require.main === module) {
603
679
  });
604
680
  }
605
681
 
606
- module.exports = { parseTar, fingerprintPublicKey };
682
+ module.exports = {
683
+ parseTar,
684
+ fingerprintPublicKey,
685
+ // Audit P P1-A: exported for tests/normalize-contract.test.js so the
686
+ // byte-stability contract can be asserted across all four normalize()
687
+ // implementations (lib/sign.js, lib/verify.js, lib/refresh-network.js,
688
+ // scripts/verify-shipped-tarball.js).
689
+ normalizeSkillBytes,
690
+ // Audit O P1-B + Q P1: exported for in-process tests of the refresh
691
+ // path's manifest envelope check.
692
+ verifyTarballManifestSignature,
693
+ canonicalManifestBytesForRefresh,
694
+ };
package/lib/sign.js CHANGED
@@ -343,9 +343,18 @@ function canonicalManifestBytes(manifest) {
343
343
  * Returns the manifest_signature object literal to splice into the
344
344
  * manifest top level.
345
345
  *
346
+ * Audit O P1-A: the previous shape included a `signed_at` ISO timestamp.
347
+ * That field was stripped from the canonical bytes before signing (via
348
+ * `delete clone.manifest_signature`), so it was NOT covered by the
349
+ * signature — an attacker who replayed a known-valid signature could
350
+ * rewrite `signed_at` to any value, lending false freshness authority to
351
+ * a stale signature. The field is now omitted entirely. Freshness signal
352
+ * lives outside the signed bytes (git-log mtime of manifest.json, npm
353
+ * publish timestamp).
354
+ *
346
355
  * @param {object} manifest
347
356
  * @param {string} privateKey PEM-encoded Ed25519 private key
348
- * @returns {{algorithm:'Ed25519', signature_base64:string, signed_at:string}}
357
+ * @returns {{algorithm:'Ed25519', signature_base64:string}}
349
358
  */
350
359
  function signCanonicalManifest(manifest, privateKey) {
351
360
  const bytes = canonicalManifestBytes(manifest);
@@ -356,7 +365,6 @@ function signCanonicalManifest(manifest, privateKey) {
356
365
  return {
357
366
  algorithm: 'Ed25519',
358
367
  signature_base64: sig.toString('base64'),
359
- signed_at: new Date().toISOString(),
360
368
  };
361
369
  }
362
370
 
package/lib/verify.js CHANGED
@@ -164,10 +164,17 @@ function signAll() {
164
164
  const manifestSig = crypto.sign(null, canonical, {
165
165
  key: privateKey, dsaEncoding: 'ieee-p1363',
166
166
  });
167
+ // Audit O P1-A: `signed_at` is intentionally OMITTED. The previous shape
168
+ // emitted a `signed_at` timestamp alongside the Ed25519 signature, but
169
+ // `signed_at` was stripped from the canonical bytes before signing — so
170
+ // an attacker could replay a known-valid signature against the same
171
+ // canonical content while rewriting `signed_at` to any value, lending
172
+ // false freshness authority to a stale signature. Operators who need
173
+ // freshness signal should consult the git-log mtime of manifest.json
174
+ // (or the npm publish timestamp), which are external to the signed bytes.
167
175
  manifest.manifest_signature = {
168
176
  algorithm: 'Ed25519',
169
177
  signature_base64: manifestSig.toString('base64'),
170
- signed_at: new Date().toISOString(),
171
178
  };
172
179
 
173
180
  fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
@@ -340,8 +347,16 @@ function verifyManifestSignature(manifest) {
340
347
  if (typeof sig.signature_base64 !== 'string') {
341
348
  return { status: 'invalid', reason: 'manifest_signature.signature_base64 missing or not a string' };
342
349
  }
343
- if (sig.algorithm && sig.algorithm !== 'Ed25519') {
344
- return { status: 'invalid', reason: `unsupported manifest_signature.algorithm: ${sig.algorithm}` };
350
+ // Audit O P1-E: require the algorithm field to be present and exactly
351
+ // 'Ed25519'. The previous form accepted a missing algorithm field
352
+ // (`if (sig.algorithm && sig.algorithm !== 'Ed25519')`) which let a
353
+ // future downgrade attacker drop the field to bait a weaker default.
354
+ // lib/sign.js always writes the field, so no legitimate consumer breaks.
355
+ if (sig.algorithm !== 'Ed25519') {
356
+ return {
357
+ status: 'invalid',
358
+ reason: `manifest_signature.algorithm must be exactly 'Ed25519' (got ${JSON.stringify(sig.algorithm)})`,
359
+ };
345
360
  }
346
361
  const publicKey = loadPublicKey();
347
362
  if (!publicKey) {
@@ -407,7 +422,14 @@ function loadManifestValidated() {
407
422
  throw new Error(`[verify] manifest_signature verification FAILED — ${sigResult.reason}. The manifest has been modified (or signed with a different key) since last sign-all. Refusing to verify any skill against this manifest.`);
408
423
  }
409
424
  if (sigResult.status === 'missing') {
410
- console.warn('[verify] WARN: manifest.json has no top-level manifest_signature field. This tarball predates v0.12.17 manifest signing; skills will still be verified but a coordinated rewrite of manifest.json could go undetected. Re-run `node lib/sign.js sign-all` to add the signature.');
425
+ // Audit O P1-D: dedupe the legacy-tarball warning. Many CLI verbs
426
+ // call loadManifestValidated() more than once per invocation; the
427
+ // previous console.warn spammed stderr per call. Node's emitWarning()
428
+ // with a stable `code` collapses repeated emissions automatically.
429
+ process.emitWarning(
430
+ 'manifest.json has no top-level manifest_signature field. This tarball predates v0.12.17 manifest signing; skills will still be verified but a coordinated rewrite of manifest.json could go undetected. Re-run `node lib/sign.js sign-all` to add the signature.',
431
+ { code: 'EXCEPTD_MANIFEST_UNSIGNED' }
432
+ );
411
433
  } else if (sigResult.status === 'no-key') {
412
434
  // Surfaced separately so the warning matches the missing-key path
413
435
  // that verifyAll() already handles — don't fail here, the verifyAll
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "last_threat_review": "2026-05-01",
54
54
  "signature": "GedX81xfe2Y/oIVTEvakZUcSPFccqsDjBibE+KbiiHezAx2EvCS4AOjlx4TO7F0iuC47G/wvOc12kMYe9iHtDw==",
55
- "signed_at": "2026-05-14T19:29:43.828Z",
55
+ "signed_at": "2026-05-14T21:23:13.037Z",
56
56
  "cwe_refs": [
57
57
  "CWE-125",
58
58
  "CWE-362",
@@ -116,7 +116,7 @@
116
116
  ],
117
117
  "last_threat_review": "2026-05-01",
118
118
  "signature": "Rz5jS554rDryT6FPVZy0PwHMCYoJQQhhNDNI9rvOptjDbsnnKtZkpVXlKke5OKmLu5fHEBaNPg856qMIZFq+Ag==",
119
- "signed_at": "2026-05-14T19:29:43.829Z",
119
+ "signed_at": "2026-05-14T21:23:13.039Z",
120
120
  "cwe_refs": [
121
121
  "CWE-1039",
122
122
  "CWE-1426",
@@ -179,7 +179,7 @@
179
179
  ],
180
180
  "last_threat_review": "2026-05-01",
181
181
  "signature": "goSMEVE6QbfcdqCEgq324TNy6rZ2mWwPA28gMPvojy8ZzGFng87hLdyvKhDMo4S3KTK1D6CaTBksAzZw4Go8Cg==",
182
- "signed_at": "2026-05-14T19:29:43.830Z",
182
+ "signed_at": "2026-05-14T21:23:13.039Z",
183
183
  "cwe_refs": [
184
184
  "CWE-22",
185
185
  "CWE-345",
@@ -225,7 +225,7 @@
225
225
  "framework_gaps": [],
226
226
  "last_threat_review": "2026-05-01",
227
227
  "signature": "XFQO+fdOb388MLxMLoTqjOHhmV/PBOPKH0nZTp5NY4uzk5iUTo6G2T/IrgZGV7/CvsuvOvaXWP8+BDllXzOpDw==",
228
- "signed_at": "2026-05-14T19:29:43.831Z"
228
+ "signed_at": "2026-05-14T21:23:13.040Z"
229
229
  },
230
230
  {
231
231
  "name": "compliance-theater",
@@ -256,7 +256,7 @@
256
256
  ],
257
257
  "last_threat_review": "2026-05-01",
258
258
  "signature": "1UD9hHv44RWc1GSvN99pmxk26xaHLC74EbJ1ndn5Sptgd7w2rU9QznqCKf7Qc18uRyNEFhqW3jHvEs9c/XgrAw==",
259
- "signed_at": "2026-05-14T19:29:43.831Z"
259
+ "signed_at": "2026-05-14T21:23:13.040Z"
260
260
  },
261
261
  {
262
262
  "name": "exploit-scoring",
@@ -285,7 +285,7 @@
285
285
  ],
286
286
  "last_threat_review": "2026-05-01",
287
287
  "signature": "4tt6UuIkq/Mi8zMhO5cydYlXyG7jKxF2MFBC34Q6Q5l3FHPhhqrL5HLOB4WFgVmq27wBbD6AgUu2czVnKa5MDQ==",
288
- "signed_at": "2026-05-14T19:29:43.831Z"
288
+ "signed_at": "2026-05-14T21:23:13.041Z"
289
289
  },
290
290
  {
291
291
  "name": "rag-pipeline-security",
@@ -322,7 +322,7 @@
322
322
  ],
323
323
  "last_threat_review": "2026-05-01",
324
324
  "signature": "4mstnPFMNhiorB7iEwqb7Jh+OCG79Mhqt2h/RwL5JbnAIPBi0Fcv0JBhQ5msEaHO4gNnpcBrdMfiDxLDwetZCw==",
325
- "signed_at": "2026-05-14T19:29:43.832Z",
325
+ "signed_at": "2026-05-14T21:23:13.041Z",
326
326
  "cwe_refs": [
327
327
  "CWE-1395",
328
328
  "CWE-1426"
@@ -379,7 +379,7 @@
379
379
  ],
380
380
  "last_threat_review": "2026-05-01",
381
381
  "signature": "FjdIy9NqQpSSMhIbyv5WhnJKrVLhO98iBfQ0AHqXw6yqXdVoWucyr729Jwhelq40oAkiBzbXi9RoVo63DHJwDw==",
382
- "signed_at": "2026-05-14T19:29:43.832Z",
382
+ "signed_at": "2026-05-14T21:23:13.041Z",
383
383
  "d3fend_refs": [
384
384
  "D3-CA",
385
385
  "D3-CSPP",
@@ -414,7 +414,7 @@
414
414
  "framework_gaps": [],
415
415
  "last_threat_review": "2026-05-01",
416
416
  "signature": "DxfXhSyoAGUo1emHh0uIIcg324ZreBYxmFdBDVAKOOuPmMlfN4RqNc/JGDSfVmMv5CjgYCUcSmkcYB0A5lk0Cg==",
417
- "signed_at": "2026-05-14T19:29:43.832Z",
417
+ "signed_at": "2026-05-14T21:23:13.041Z",
418
418
  "cwe_refs": [
419
419
  "CWE-1188"
420
420
  ]
@@ -442,7 +442,7 @@
442
442
  "framework_gaps": [],
443
443
  "last_threat_review": "2026-05-01",
444
444
  "signature": "xwlad/p5XlICc76KqrdWpEpBgwx1D87oM5qlccRQ5LKC/o0pr8vQ8LN3WLiiziBChplwNbruiIN6UETniZpjCg==",
445
- "signed_at": "2026-05-14T19:29:43.833Z"
445
+ "signed_at": "2026-05-14T21:23:13.042Z"
446
446
  },
447
447
  {
448
448
  "name": "global-grc",
@@ -474,7 +474,7 @@
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-01",
476
476
  "signature": "7yVjZkanFMKDQqXdX4B/7oLc2Rz72xHC1zscYd8F/+e5UAbR7ikK8Bn5EKZt3aBEOhHPAviSQNCMxpZD9U00CA==",
477
- "signed_at": "2026-05-14T19:29:43.834Z"
477
+ "signed_at": "2026-05-14T21:23:13.043Z"
478
478
  },
479
479
  {
480
480
  "name": "zeroday-gap-learn",
@@ -501,7 +501,7 @@
501
501
  "framework_gaps": [],
502
502
  "last_threat_review": "2026-05-01",
503
503
  "signature": "pKv1b8JAj1ldwR2KGccvjUKqlor2KNXXzxkKypkv+4O8WbqY7v8fWlbFe1F36OJrL2xYJdnZL5mJzqjYVHLRCg==",
504
- "signed_at": "2026-05-14T19:29:43.834Z"
504
+ "signed_at": "2026-05-14T21:23:13.043Z"
505
505
  },
506
506
  {
507
507
  "name": "pqc-first",
@@ -553,7 +553,7 @@
553
553
  ],
554
554
  "last_threat_review": "2026-05-01",
555
555
  "signature": "V+qn5FqUlETfsEjvvi6jZGuQdqLFtFejfgPA6KSYxSlBXBTbOBXP3BGk5S+ba9akIzgbKh1j9VGB1MqsIt56DA==",
556
- "signed_at": "2026-05-14T19:29:43.834Z",
556
+ "signed_at": "2026-05-14T21:23:13.043Z",
557
557
  "cwe_refs": [
558
558
  "CWE-327"
559
559
  ],
@@ -600,7 +600,7 @@
600
600
  ],
601
601
  "last_threat_review": "2026-05-01",
602
602
  "signature": "UayHLLWXAkhnLPaPRsgDpAyE8FGk1tuG1/DYyhw84Uv4tfCKXMamsAhXHOyMIosQfsJq5ZHYVXZz0bYNpnlvDw==",
603
- "signed_at": "2026-05-14T19:29:43.835Z"
603
+ "signed_at": "2026-05-14T21:23:13.044Z"
604
604
  },
605
605
  {
606
606
  "name": "security-maturity-tiers",
@@ -637,7 +637,7 @@
637
637
  ],
638
638
  "last_threat_review": "2026-05-01",
639
639
  "signature": "zjq6ACAHD46xvhvQJKlrCPh5xDCuBuIWBI+QJB8RxcudpC7p7I1pqv+BY8DZdsAgU4tquCU8KC+xlduMIk3/DQ==",
640
- "signed_at": "2026-05-14T19:29:43.835Z",
640
+ "signed_at": "2026-05-14T21:23:13.044Z",
641
641
  "cwe_refs": [
642
642
  "CWE-1188"
643
643
  ]
@@ -672,7 +672,7 @@
672
672
  "framework_gaps": [],
673
673
  "last_threat_review": "2026-05-11",
674
674
  "signature": "/lGgWehCMQUXjI6w4FUa+5wrbyRnct+txvVcXA+D2/ZEkoJKh+J/psO3j5HPf7Hpv+Y5SmkH71CoO+9qilyVDQ==",
675
- "signed_at": "2026-05-14T19:29:43.835Z"
675
+ "signed_at": "2026-05-14T21:23:13.044Z"
676
676
  },
677
677
  {
678
678
  "name": "attack-surface-pentest",
@@ -743,7 +743,7 @@
743
743
  "PTES revision incorporating AI-surface enumeration"
744
744
  ],
745
745
  "signature": "rh+/cr+wTcEmBwrGscBni/jXpxjjYP91pUKDFIGkahZpw+nghCM/3aLKFf5RFRnl3JKTyBRywIrYhUH1YuSlDw==",
746
- "signed_at": "2026-05-14T19:29:43.836Z"
746
+ "signed_at": "2026-05-14T21:23:13.044Z"
747
747
  },
748
748
  {
749
749
  "name": "fuzz-testing-strategy",
@@ -803,7 +803,7 @@
803
803
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
804
804
  ],
805
805
  "signature": "+ELdD+1AY5DymBitH7wU65CS60NY1nDoLowJAFn7cE5Gr/5jy9BTkyxsm7PEXaSlXWMOkTf/HQ+uyzyxUVD/Bw==",
806
- "signed_at": "2026-05-14T19:29:43.836Z"
806
+ "signed_at": "2026-05-14T21:23:13.045Z"
807
807
  },
808
808
  {
809
809
  "name": "dlp-gap-analysis",
@@ -878,7 +878,7 @@
878
878
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
879
879
  ],
880
880
  "signature": "/BCBGUVjGs1RZzqXfElxBWB8UoD4+MY2G1YekdWsTDbMcHvt3NZJf0/JcqdYHOsEhFQ21NEz3w3+6tmQ8htKDw==",
881
- "signed_at": "2026-05-14T19:29:43.837Z"
881
+ "signed_at": "2026-05-14T21:23:13.045Z"
882
882
  },
883
883
  {
884
884
  "name": "supply-chain-integrity",
@@ -955,7 +955,7 @@
955
955
  "OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
956
956
  ],
957
957
  "signature": "mySOkGScsNPtEZcHg42EKcvUSBzADIB9mSlNe0L1yPllrB/83ypBj6cCERRw9ql+rtrNxapyc6Do+nCz7E5rDg==",
958
- "signed_at": "2026-05-14T19:29:43.837Z"
958
+ "signed_at": "2026-05-14T21:23:13.045Z"
959
959
  },
960
960
  {
961
961
  "name": "defensive-countermeasure-mapping",
@@ -1012,7 +1012,7 @@
1012
1012
  ],
1013
1013
  "last_threat_review": "2026-05-11",
1014
1014
  "signature": "XZigwq8X/csfrdG10O6Q1V5q0zUqSQGd3QrjRKkZ4fkaodG4mZahYuIQqxc8rU9jjtGAm9LtBXYB+I5csqj9Bw==",
1015
- "signed_at": "2026-05-14T19:29:43.837Z"
1015
+ "signed_at": "2026-05-14T21:23:13.046Z"
1016
1016
  },
1017
1017
  {
1018
1018
  "name": "identity-assurance",
@@ -1079,7 +1079,7 @@
1079
1079
  "d3fend_refs": [],
1080
1080
  "last_threat_review": "2026-05-11",
1081
1081
  "signature": "k0HrsZMBxiPWB1jl4dRwhv/R5IsqbZ+SLDv1Jx3/sRl51JyXjtm8vyogTNhSwsl5/IkaRakqIPJFRFRl5h/9CQ==",
1082
- "signed_at": "2026-05-14T19:29:43.838Z"
1082
+ "signed_at": "2026-05-14T21:23:13.046Z"
1083
1083
  },
1084
1084
  {
1085
1085
  "name": "ot-ics-security",
@@ -1135,7 +1135,7 @@
1135
1135
  "d3fend_refs": [],
1136
1136
  "last_threat_review": "2026-05-11",
1137
1137
  "signature": "oHxjumOhk8y86WcwhAX8sSWIlPzt60KfTMn4DCJLeRrrQd5+i54fVADKAdZ3vOqfDN+DexO0uX4f5dLPtacRCQ==",
1138
- "signed_at": "2026-05-14T19:29:43.838Z"
1138
+ "signed_at": "2026-05-14T21:23:13.046Z"
1139
1139
  },
1140
1140
  {
1141
1141
  "name": "coordinated-vuln-disclosure",
@@ -1187,7 +1187,7 @@
1187
1187
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1188
1188
  ],
1189
1189
  "signature": "UCiNjncvhkZItmLQA/Sm1/NCsOiLMwdCjfUw+067v4NIxhaMMaqRrAeD3KgMyEtov7m2Hq2kfwYSt5+DQsYDCQ==",
1190
- "signed_at": "2026-05-14T19:29:43.838Z"
1190
+ "signed_at": "2026-05-14T21:23:13.047Z"
1191
1191
  },
1192
1192
  {
1193
1193
  "name": "threat-modeling-methodology",
@@ -1237,7 +1237,7 @@
1237
1237
  "PASTA v2 updates incorporating AI/ML application threats"
1238
1238
  ],
1239
1239
  "signature": "V9kl8Cf8UMjNFyn3D/fSyhWHLeXWlx3WV/jT9jdF9SrjfDqymimuTt2o91cZ2FOEJndAH9V0JGXB13Ohz8K4CQ==",
1240
- "signed_at": "2026-05-14T19:29:43.839Z"
1240
+ "signed_at": "2026-05-14T21:23:13.047Z"
1241
1241
  },
1242
1242
  {
1243
1243
  "name": "webapp-security",
@@ -1311,7 +1311,7 @@
1311
1311
  "d3fend_refs": [],
1312
1312
  "last_threat_review": "2026-05-11",
1313
1313
  "signature": "csC9KRA3ExCSK77+UF6gj0OFPPZzOvuPxQtKEoaUZmLvn3V37My4IpbUjXAN2ZavTHqyFG9yQNfq3ELZeSJ7Cg==",
1314
- "signed_at": "2026-05-14T19:29:43.839Z"
1314
+ "signed_at": "2026-05-14T21:23:13.047Z"
1315
1315
  },
1316
1316
  {
1317
1317
  "name": "ai-risk-management",
@@ -1361,7 +1361,7 @@
1361
1361
  "d3fend_refs": [],
1362
1362
  "last_threat_review": "2026-05-11",
1363
1363
  "signature": "P2D++lB5hea3oi2vl9mf8C7N+E7zASoqt1v4tjKxtaTeb+U0UARgMOaZsoK/sO9TT/PG/au14Rl4EFxv+Xi1BA==",
1364
- "signed_at": "2026-05-14T19:29:43.839Z"
1364
+ "signed_at": "2026-05-14T21:23:13.048Z"
1365
1365
  },
1366
1366
  {
1367
1367
  "name": "sector-healthcare",
@@ -1421,7 +1421,7 @@
1421
1421
  "d3fend_refs": [],
1422
1422
  "last_threat_review": "2026-05-11",
1423
1423
  "signature": "BDuLcpTeFp2BNSf1q4rYOhYKNhlgd3o5RZ0Uw9xW5olyYxPbZSgqekQ+6Ggaec09s7y6sqR37GS0vuAMdbrdDQ==",
1424
- "signed_at": "2026-05-14T19:29:43.840Z"
1424
+ "signed_at": "2026-05-14T21:23:13.048Z"
1425
1425
  },
1426
1426
  {
1427
1427
  "name": "sector-financial",
@@ -1502,7 +1502,7 @@
1502
1502
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1503
1503
  ],
1504
1504
  "signature": "4IUJePr6XbE1Ns+cPvEFAVgrwdHLImuxdPYiilurxM2SmJym1itRC1prFMcuT6Kh6e1clYXwlzflcKm/eikyDA==",
1505
- "signed_at": "2026-05-14T19:29:43.840Z"
1505
+ "signed_at": "2026-05-14T21:23:13.048Z"
1506
1506
  },
1507
1507
  {
1508
1508
  "name": "sector-federal-government",
@@ -1571,7 +1571,7 @@
1571
1571
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1572
1572
  ],
1573
1573
  "signature": "nMsyJ+rp5fM8/VjC7zsZyDjOC4hpxB+noT1VX7W0HBlq5t3SY56cwOGApwES/kBcCuf4qexKY376OxUr93zvCQ==",
1574
- "signed_at": "2026-05-14T19:29:43.841Z"
1574
+ "signed_at": "2026-05-14T21:23:13.049Z"
1575
1575
  },
1576
1576
  {
1577
1577
  "name": "sector-energy",
@@ -1636,7 +1636,7 @@
1636
1636
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1637
1637
  ],
1638
1638
  "signature": "L1moEqEGkBkqY/3ohJcfqrlJn40UurDCyb2MOP/IwTAeZD+QbVZ17/drdsydkJ6qSXPiyiE6u8HDfZsDS13NBQ==",
1639
- "signed_at": "2026-05-14T19:29:43.841Z"
1639
+ "signed_at": "2026-05-14T21:23:13.049Z"
1640
1640
  },
1641
1641
  {
1642
1642
  "name": "api-security",
@@ -1705,7 +1705,7 @@
1705
1705
  "d3fend_refs": [],
1706
1706
  "last_threat_review": "2026-05-11",
1707
1707
  "signature": "ad1pHD4QQ8uXkhrzqLuWgnDpESOapzx3qGFchU9rxiX1aeLQkYKwpDzqIItFq82B5xjNsW7g5jXlF1sgK2HmCA==",
1708
- "signed_at": "2026-05-14T19:29:43.842Z"
1708
+ "signed_at": "2026-05-14T21:23:13.050Z"
1709
1709
  },
1710
1710
  {
1711
1711
  "name": "cloud-security",
@@ -1786,7 +1786,7 @@
1786
1786
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1787
1787
  ],
1788
1788
  "signature": "UEn0305KAEqIfYOdzadLBdPG/PJ+3sJ/8ubvPFNcXfqXp2uOWTfqGUqY65PApA992VEEa1RBQt5R7Nyhd/OjDQ==",
1789
- "signed_at": "2026-05-14T19:29:43.842Z"
1789
+ "signed_at": "2026-05-14T21:23:13.051Z"
1790
1790
  },
1791
1791
  {
1792
1792
  "name": "container-runtime-security",
@@ -1848,7 +1848,7 @@
1848
1848
  "d3fend_refs": [],
1849
1849
  "last_threat_review": "2026-05-11",
1850
1850
  "signature": "4easZDYn25XK4E9MRnwnZohG3xdYMmOLlPznNVmr1ykNfB+343+ooj+R0quG8uEV/IqbTQpR1ink35K6jCghCg==",
1851
- "signed_at": "2026-05-14T19:29:43.842Z"
1851
+ "signed_at": "2026-05-14T21:23:13.051Z"
1852
1852
  },
1853
1853
  {
1854
1854
  "name": "mlops-security",
@@ -1919,7 +1919,7 @@
1919
1919
  "MITRE ATLAS v5.2 — track AML.T0010 sub-technique expansion and any new MLOps-pipeline-specific TTPs"
1920
1920
  ],
1921
1921
  "signature": "chbPWzjfx92OjEAwMIm+J4GObxy8uwTahNBvbhMfYL7vTAJe/lf2BaW8wUpchpMIwYL0985A/+WykH8zmk/DBA==",
1922
- "signed_at": "2026-05-14T19:29:43.843Z"
1922
+ "signed_at": "2026-05-14T21:23:13.051Z"
1923
1923
  },
1924
1924
  {
1925
1925
  "name": "incident-response-playbook",
@@ -1981,7 +1981,7 @@
1981
1981
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
1982
1982
  ],
1983
1983
  "signature": "3V7kvM5cxXdCBoMnjvOoTvT3zD+/yZEBHYgiunYQe8tBm+vVnS4jCz1Nzv/ymePIfbYDo/PlzKeGTWStSsGiAg==",
1984
- "signed_at": "2026-05-14T19:29:43.843Z"
1984
+ "signed_at": "2026-05-14T21:23:13.052Z"
1985
1985
  },
1986
1986
  {
1987
1987
  "name": "email-security-anti-phishing",
@@ -2034,7 +2034,7 @@
2034
2034
  "d3fend_refs": [],
2035
2035
  "last_threat_review": "2026-05-11",
2036
2036
  "signature": "RiCryJEd66T2NNcSo/mZTd3sGWDycE3C37guLJanLdVL5co35DrPFmIl8qy3ZM/y+Wzg5vpny8VKgr1//1/bCA==",
2037
- "signed_at": "2026-05-14T19:29:43.843Z"
2037
+ "signed_at": "2026-05-14T21:23:13.052Z"
2038
2038
  },
2039
2039
  {
2040
2040
  "name": "age-gates-child-safety",
@@ -2102,12 +2102,11 @@
2102
2102
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2103
2103
  ],
2104
2104
  "signature": "MMWvg3lIf5ygm31zyf1E43t3W9MfRbMBBPrqlj1wOa8AxVJL8LICnAXfmyJ/TNJXwpF+rfZeDdoxXkql8wmtBA==",
2105
- "signed_at": "2026-05-14T19:29:43.844Z"
2105
+ "signed_at": "2026-05-14T21:23:13.053Z"
2106
2106
  }
2107
2107
  ],
2108
2108
  "manifest_signature": {
2109
2109
  "algorithm": "Ed25519",
2110
- "signature_base64": "TD/DHSjapE7OQbmJ7a4GMh+35vcyhWvyQU0kjXZZWByNZJsvUDPlyYy0bEiz6/BylGv0QLpTsT0iQWHGPu5ECA==",
2111
- "signed_at": "2026-05-14T19:29:43.845Z"
2110
+ "signature_base64": "Ql9tfxW2Qoi/Zjfhwgm0o2TrefcyaOusalb9vMbfwhHPiNlAsPwDs5iFXA4oI3ZwNJ72GE2FY54Wtmq/QU7cDA=="
2112
2111
  }
2113
2112
  }
@@ -118,6 +118,16 @@ function _shouldBootstrapFire(key, intervalMs) {
118
118
  * @returns {Function} Call to stop further firings.
119
119
  */
120
120
  function scheduleEvery(intervalMs, handler) {
121
+ // T P1-4: lower-bound guard. v0.12.12 added the INT32 overflow clamp
122
+ // (upper bound) but never asserted intervalMs > 0. `scheduleEvery(0, fn)`
123
+ // would set a 0ms interval that fires ~10k times per second; negatives
124
+ // (-100) coerce the same way and NaN drives setInterval into a 1ms tick.
125
+ // All three exhaust the event loop. Refuse the call rather than silently
126
+ // floor — the scheduler is a long-lived primitive and a footgun here
127
+ // poisons every periodic task in the watcher.
128
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
129
+ throw new RangeError(`scheduleEvery: intervalMs must be a positive finite number, got ${intervalMs}`);
130
+ }
121
131
  const startedAt = Date.now();
122
132
  let lastFired = startedAt;
123
133
  const tick = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:f814e8e0-7189-4a08-8117-b35a35ae4f30",
4
+ "serialNumber": "urn:uuid:4ae17d1f-ac0e-41e3-95eb-0b58ced8b08d",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-14T19:29:45.003Z",
7
+ "timestamp": "2026-05-14T21:23:13.962Z",
8
8
  "tools": [
9
9
  {
10
10
  "name": "hand-written",
@@ -13,10 +13,10 @@
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.18",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.20",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.12.18",
19
+ "version": "0.12.20",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,11 +25,11 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.18",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.20",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.18"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.20"
33
33
  },
34
34
  {
35
35
  "type": "vcs",
@@ -177,6 +177,11 @@ const GATES = [
177
177
  args: [path.join(ROOT, "lib", "validate-playbooks.js")],
178
178
  ciJobName: "Validate playbooks",
179
179
  informational: true,
180
+ // audit Y F30: cap informational acceptance at exit 1 so a CRASH (137 OOM,
181
+ // 139 SIGSEGV, 134 SIGABRT, etc.) surfaces as a real failure instead of
182
+ // being absorbed under the informational bucket. Matches the
183
+ // forward-watch gate (line ~111) which already pins the same ceiling.
184
+ informationalMaxExitCode: 1,
180
185
  },
181
186
  ];
182
187