@blamejs/exceptd-skills 0.9.3 → 0.9.5

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.
@@ -152,16 +152,34 @@ function cryptoScan() {
152
152
  const [major, minor] = version.split('.').map(Number);
153
153
  const isPqcReady = major > 3 || (major === 3 && minor >= 5);
154
154
 
155
+ // Probe the full NIST PQC suite + stateful hash signatures.
156
+ // Reports per-algo via boolean flags so downstream callers can
157
+ // decide which gaps matter for their threat model (ML-KEM for
158
+ // HNDL exposure, LMS/XMSS for firmware signing, etc.).
159
+ const pqc = probePqcAlgorithms();
160
+ const pqcDetail = [
161
+ `ML-KEM=${pqc.ml_kem ? 'avail' : 'missing'}`, // FIPS 203
162
+ `ML-DSA=${pqc.ml_dsa ? 'avail' : 'missing'}`, // FIPS 204
163
+ `SLH-DSA=${pqc.slh_dsa ? 'avail' : 'missing'}`, // FIPS 205
164
+ `FN-DSA=${pqc.fn_dsa ? 'avail' : 'missing'}`, // FIPS 206 draft (Falcon)
165
+ `HQC=${pqc.hqc ? 'avail' : 'missing'}`, // alternate KEM (March 2025)
166
+ `LMS=${pqc.lms ? 'avail' : 'missing'}`, // RFC 8554 (firmware)
167
+ `XMSS=${pqc.xmss ? 'avail' : 'missing'}`, // RFC 8391
168
+ ].join(' ');
169
+
155
170
  findings.push({
156
171
  domain: 'crypto',
157
172
  signal: 'openssl_version',
158
173
  value: version,
159
174
  pqc_ready: isPqcReady,
160
- severity: isPqcReady ? 'info' : 'high',
175
+ pqc_algorithms: pqc,
176
+ severity: isPqcReady && pqc.ml_kem ? 'info' : (isPqcReady ? 'medium' : 'high'),
161
177
  skill_hint: 'pqc-first',
162
178
  action_required: isPqcReady
163
- ? `OpenSSL ${version} — PQC-capable. Verify ML-KEM/ML-DSA algorithm availability.`
164
- : `OpenSSL ${version} — below 3.5. Upgrade required for PQC support (ML-KEM, ML-DSA, SLH-DSA).`
179
+ ? (pqc.ml_kem
180
+ ? `OpenSSL ${version} — PQC-capable. Probed: ${pqcDetail}.`
181
+ : `OpenSSL ${version} — claims PQC support but probe found no ML-KEM. Check provider config; may need OQS-Provider or Node 24 crypto.kemEncapsulate. Probed: ${pqcDetail}.`)
182
+ : `OpenSSL ${version} — below 3.5. Upgrade required for PQC support (ML-KEM, ML-DSA, SLH-DSA, FN-DSA, HQC, LMS, XMSS).`
165
183
  });
166
184
  }
167
185
 
@@ -296,6 +314,153 @@ function safeExecFile(cmd, args) {
296
314
  }
297
315
  }
298
316
 
317
+ /**
318
+ * Probe runtime for PQC algorithm availability across the full
319
+ * emerging-standards landscape. Returns boolean flags per algorithm
320
+ * + a `provider_hint` indicating which surface confirmed each one.
321
+ *
322
+ * --- NIST PQC finalized (FIPS 203/204/205, 2024) ---
323
+ * ml_kem ML-KEM (Kyber) — FIPS 203, key encapsulation
324
+ * ml_dsa ML-DSA (Dilithium) — FIPS 204, signatures
325
+ * slh_dsa SLH-DSA (SPHINCS+) — FIPS 205, stateless hash sigs
326
+ *
327
+ * --- NIST PQC draft / alternate (2025+) ---
328
+ * fn_dsa FN-DSA (Falcon) — FIPS 206 draft, compact lattice sigs
329
+ * hqc HQC — alternate KEM selected March 2025
330
+ *
331
+ * --- NIST PQC Round-4 alternates (still relevant in niche / archival) ---
332
+ * frodo FrodoKEM — conservative lattice KEM
333
+ * ntru NTRU / NTRU-Prime / sNTRU — original lattice KEM family
334
+ * mceliece Classic McEliece — code-based, long-term archival use
335
+ * bike BIKE — code-based KEM, OQS-Provider exposed
336
+ *
337
+ * --- NIST additional signature on-ramp (2023+ Round 2) ---
338
+ * hawk HAWK — NTRU-based lattice signatures
339
+ * mayo MAYO — multivariate
340
+ * sqisign SQIsign — isogeny-based, small signatures
341
+ * cross CROSS — code-based
342
+ * uov UOV / SNOVA — Unbalanced Oil & Vinegar multivariate
343
+ * sdith SDitH — code-based (Syndrome Decoding in the Head)
344
+ * mirath MIRATH — code-based (rank metric)
345
+ * faest FAEST — symmetric-key/AES-based signatures
346
+ * perk PERK — code-based (Permuted Kernel Problem)
347
+ *
348
+ * --- Stateful hash signatures (RFC 8391 / 8554) ---
349
+ * lms LMS — Leighton-Micali Signatures (firmware)
350
+ * xmss XMSS — eXtended Merkle Signature Scheme
351
+ * hss HSS — Hierarchical Signature System
352
+ *
353
+ * --- IETF hybrid / composite sigs (emerging RFC drafts) ---
354
+ * composite_sig Composite Signatures (e.g. RSA+ML-DSA, ECDSA+ML-DSA)
355
+ * composite_kem Composite KEMs (e.g. X25519+ML-KEM)
356
+ */
357
+ function probePqcAlgorithms() {
358
+ const result = {
359
+ // NIST finalized
360
+ ml_kem: false, ml_dsa: false, slh_dsa: false,
361
+ // NIST draft / alternate
362
+ fn_dsa: false, hqc: false,
363
+ // NIST Round-4 / niche
364
+ frodo: false, ntru: false, mceliece: false, bike: false,
365
+ // NIST signature on-ramp (Round 2)
366
+ hawk: false, mayo: false, sqisign: false, cross: false,
367
+ uov: false, sdith: false, mirath: false, faest: false, perk: false,
368
+ // Stateful hash sigs
369
+ lms: false, xmss: false, hss: false,
370
+ // IETF composite / hybrid
371
+ composite_sig: false, composite_kem: false,
372
+ provider_hint: {},
373
+ };
374
+ const crypto = require('crypto');
375
+
376
+ // Pattern table — values are regexes matching common spellings
377
+ // emitted by Node / OpenSSL / OQS-Provider / Bouncy Castle. Tested
378
+ // against algorithm-list output specifically (no English-prose
379
+ // false positives expected; the lists contain only algo names).
380
+ const PATTERNS = {
381
+ // NIST finalized
382
+ ml_kem: /\b(ml-?kem|kyber)\b/i,
383
+ ml_dsa: /\b(ml-?dsa|dilithium)\b/i,
384
+ slh_dsa: /\b(slh-?dsa|sphincs(?:\+|plus)?)\b/i,
385
+ // NIST draft / alternate
386
+ fn_dsa: /\b(fn-?dsa|falcon-?\d{3,4})\b/i,
387
+ hqc: /\bhqc(-?\d+)?\b/i,
388
+ // NIST Round-4 / niche
389
+ frodo: /\bfrodo(-?\d+)?(kem)?\b/i,
390
+ ntru: /\b(s?ntru(-?prime)?(-?\d+)?|hrss\d*|hps\d+)\b/i,
391
+ mceliece: /\b(classic-?)?mceliece(-?\d+)?\b/i,
392
+ bike: /\bbike(-?l?\d+)?\b/i,
393
+ // NIST signature on-ramp (Round 2, 2024+)
394
+ hawk: /\bhawk(-?\d+)?\b/i,
395
+ mayo: /\bmayo(-?\d+)?\b/i,
396
+ sqisign: /\bsqi-?sign(?:hd)?\b/i,
397
+ cross: /\bcross-?(rsdp|rsdpg)?-?(small|fast|balanced)?-?[lns]?\d*\b/i,
398
+ uov: /\b(uov|snova)(-?\d+)?\b/i,
399
+ sdith: /\bsd-?i?t?h(-?gf\d+)?(-?cat\d+)?\b/i,
400
+ mirath: /\bmirath(-?\d+)?\b/i,
401
+ faest: /\bfaest(-em)?(-?\d+)?\b/i,
402
+ perk: /\bperk-?[ls]?-?\d*\b/i,
403
+ // Stateful hash signatures
404
+ lms: /\blms(?!-sha)\b/i,
405
+ xmss: /\bxmss(\^?mt)?\b/i,
406
+ hss: /\bhss(?!-sha)\b/i,
407
+ // Composite / hybrid (IETF drafts)
408
+ composite_sig: /\bcomposite-?(sig|signature)\b|\bhybrid-?sig\b|\b(rsa|ecdsa|eddsa)-?(\+|with)-?(ml-?dsa|dilithium|slh-?dsa|sphincs|falcon|fn-?dsa)\b/i,
409
+ composite_kem: /\bcomposite-?kem\b|\b(x25519|x448|secp\d+)-?(\+|with)-?(ml-?kem|kyber)\b|\bml-?kem-?(\+|with)-?(x25519|x448)\b/i,
410
+ };
411
+
412
+ function record(algo, source) {
413
+ if (!result[algo]) {
414
+ result[algo] = true;
415
+ result.provider_hint[algo] = source;
416
+ }
417
+ }
418
+
419
+ // Channel 1 — Node's crypto APIs (no shellouts). Node 24+ exposes
420
+ // crypto.kemEncapsulate as an experimental ML-KEM gateway; its
421
+ // presence is itself an ML-KEM availability signal.
422
+ try {
423
+ if (typeof crypto.kemEncapsulate === 'function') {
424
+ record('ml_kem', 'node:crypto.kemEncapsulate');
425
+ }
426
+ const allNames = [
427
+ ...(crypto.getCurves ? crypto.getCurves() : []),
428
+ ...(crypto.getHashes ? crypto.getHashes() : []),
429
+ ...(crypto.getCiphers ? crypto.getCiphers() : []),
430
+ ];
431
+ for (const name of allNames) {
432
+ for (const [algo, re] of Object.entries(PATTERNS)) {
433
+ if (re.test(name)) record(algo, `node:${name}`);
434
+ }
435
+ }
436
+ } catch (_) { /* probe failure → fall through to openssl */ }
437
+
438
+ // Channel 2 — `openssl list` enumerations. KEMs and signature
439
+ // algorithms split across two list commands; OQS-Provider exposes
440
+ // the on-ramp / niche algos when installed.
441
+ const kemList = safeExecFile('openssl', ['list', '-kem-algorithms']);
442
+ if (kemList) {
443
+ const kemAlgos = ['ml_kem', 'hqc', 'frodo', 'ntru', 'mceliece', 'bike', 'composite_kem'];
444
+ for (const algo of kemAlgos) {
445
+ if (PATTERNS[algo].test(kemList)) record(algo, 'openssl:list -kem-algorithms');
446
+ }
447
+ }
448
+ const sigList = safeExecFile('openssl', ['list', '-signature-algorithms']);
449
+ if (sigList) {
450
+ const sigAlgos = [
451
+ 'ml_dsa', 'slh_dsa', 'fn_dsa',
452
+ 'hawk', 'mayo', 'sqisign', 'cross', 'uov', 'sdith', 'mirath', 'faest', 'perk',
453
+ 'lms', 'xmss', 'hss',
454
+ 'composite_sig',
455
+ ];
456
+ for (const algo of sigAlgos) {
457
+ if (PATTERNS[algo].test(sigList)) record(algo, 'openssl:list -signature-algorithms');
458
+ }
459
+ }
460
+
461
+ return result;
462
+ }
463
+
299
464
  function probeTls() {
300
465
  const result = spawnSync('openssl', ['s_client', '-connect', 'google.com:443', '-brief'], {
301
466
  input: '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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",
@@ -37,7 +37,7 @@
37
37
  "license": "Apache-2.0",
38
38
  "author": "blamejs contributors",
39
39
  "engines": {
40
- "node": ">=24.0.0 <25.0.0"
40
+ "node": ">=24.0.0"
41
41
  },
42
42
  "bin": {
43
43
  "exceptd": "bin/exceptd.js"
package/sbom.cdx.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:c19faf5e-24fc-445d-a9aa-b0cae794c5d7",
4
+ "serialNumber": "urn:uuid:bb7ee40b-d15e-4cce-9862-51703a54b2c0",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T03:22:57.634Z",
7
+ "timestamp": "2026-05-12T03:50:55.384Z",
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.9.3",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.9.5",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.9.3",
19
+ "version": "0.9.5",
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.9.3",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.9.5",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.9.3"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.9.5"
33
33
  },
34
34
  {
35
35
  "type": "vcs",
@@ -20,13 +20,15 @@
20
20
  const fs = require("fs");
21
21
  const path = require("path");
22
22
 
23
- function currencyScore(daysSinceReview, forwardWatchCount) {
23
+ function currencyScore(daysSinceReview, _forwardWatchCount) {
24
+ // See orchestrator/pipeline.js — forward_watch count no longer
25
+ // affects currency score (it's a maintenance signal, not staleness).
26
+ // Param retained for ABI compatibility with callers.
24
27
  let score = 100;
25
28
  if (daysSinceReview > 180) score -= 30;
26
29
  else if (daysSinceReview > 90) score -= 20;
27
30
  else if (daysSinceReview > 60) score -= 10;
28
31
  else if (daysSinceReview > 30) score -= 5;
29
- score -= forwardWatchCount * 5;
30
32
  return Math.max(0, score);
31
33
  }
32
34
 
@@ -99,7 +101,7 @@ function buildCurrency({ root, manifest, skills }) {
99
101
  schema_version: "1.0.0",
100
102
  reference_date: ref,
101
103
  note: "Pre-computed skill currency snapshot. Reference date is manifest.threat_review_date (deterministic). Re-runs of build-indexes against the same inputs produce byte-identical output. The orchestrator `currency` command produces a real-time view against today's date.",
102
- decay_formula: "100 base; -30/-20/-10/-5 at 180/90/60/30-day thresholds; -5 per forward_watch entry. Label thresholds: ≥90 current, ≥70 acceptable, ≥50 stale, <50 critical_stale.",
104
+ decay_formula: "100 base; -30/-20/-10/-5 at 180/90/60/30-day thresholds. forward_watch count does NOT affect the score (it's a maintenance signal, not a staleness one). Label thresholds: ≥90 current, ≥70 acceptable, ≥50 stale, <50 critical_stale.",
103
105
  },
104
106
  summary,
105
107
  skills: rows,