@blamejs/exceptd-skills 0.9.4 → 0.10.0

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.
@@ -19,12 +19,34 @@ const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(__dirname, '..', 'dat
19
19
 
20
20
  /**
21
21
  * Run all scanners and return consolidated findings.
22
+ *
23
+ * DEPRECATED in v0.10.0. This is the pre-seven-phase legacy scanner that
24
+ * shells out from Node — kept as a safety-net path for non-AI operators.
25
+ * New code should call the playbook runner instead:
26
+ *
27
+ * const runner = require('../lib/playbook-runner');
28
+ * const result = await runner.run('kernel', 'all-catalogued-kernel-cves', agentSubmission);
29
+ *
30
+ * The runner emits seven-phase findings (govern through close) with full
31
+ * GRC closure: CSAF-2.0 evidence bundles, jurisdiction-aware notification
32
+ * deadlines, auditor-ready exception language, regression schedules.
33
+ * `exceptd scan` will be removed in v1.0.
34
+ *
22
35
  * @returns {{ timestamp: string, host: object, findings: object[], summary: object }}
23
36
  */
24
37
  async function scan() {
25
38
  const timestamp = new Date().toISOString();
26
39
  const findings = [];
27
40
 
41
+ // Deprecation banner — surfaces via stderr so JSON consumers aren't broken.
42
+ if (process.stderr.isTTY && !process.env.EXCEPTD_SUPPRESS_DEPRECATION) {
43
+ process.stderr.write(
44
+ '[scan] DEPRECATION: `exceptd scan` is legacy. Prefer `exceptd plan` + `exceptd run <playbook>` ' +
45
+ '(seven-phase contract with full GRC closure). See AGENTS.md "Seven-phase playbook contract". ' +
46
+ 'Suppress this notice with EXCEPTD_SUPPRESS_DEPRECATION=1.\n'
47
+ );
48
+ }
49
+
28
50
  const host = hostInfo();
29
51
  findings.push(...kernelScan());
30
52
  findings.push(...mcpScan());
@@ -33,7 +55,7 @@ async function scan() {
33
55
  findings.push(...frameworkScan());
34
56
 
35
57
  const summary = summarize(findings);
36
- return { timestamp, host, findings, summary };
58
+ return { timestamp, host, findings, summary, _deprecation: 'Use `exceptd plan` + `exceptd run <playbook>` for the seven-phase contract.' };
37
59
  }
38
60
 
39
61
  /**
@@ -152,16 +174,34 @@ function cryptoScan() {
152
174
  const [major, minor] = version.split('.').map(Number);
153
175
  const isPqcReady = major > 3 || (major === 3 && minor >= 5);
154
176
 
177
+ // Probe the full NIST PQC suite + stateful hash signatures.
178
+ // Reports per-algo via boolean flags so downstream callers can
179
+ // decide which gaps matter for their threat model (ML-KEM for
180
+ // HNDL exposure, LMS/XMSS for firmware signing, etc.).
181
+ const pqc = probePqcAlgorithms();
182
+ const pqcDetail = [
183
+ `ML-KEM=${pqc.ml_kem ? 'avail' : 'missing'}`, // FIPS 203
184
+ `ML-DSA=${pqc.ml_dsa ? 'avail' : 'missing'}`, // FIPS 204
185
+ `SLH-DSA=${pqc.slh_dsa ? 'avail' : 'missing'}`, // FIPS 205
186
+ `FN-DSA=${pqc.fn_dsa ? 'avail' : 'missing'}`, // FIPS 206 draft (Falcon)
187
+ `HQC=${pqc.hqc ? 'avail' : 'missing'}`, // alternate KEM (March 2025)
188
+ `LMS=${pqc.lms ? 'avail' : 'missing'}`, // RFC 8554 (firmware)
189
+ `XMSS=${pqc.xmss ? 'avail' : 'missing'}`, // RFC 8391
190
+ ].join(' ');
191
+
155
192
  findings.push({
156
193
  domain: 'crypto',
157
194
  signal: 'openssl_version',
158
195
  value: version,
159
196
  pqc_ready: isPqcReady,
160
- severity: isPqcReady ? 'info' : 'high',
197
+ pqc_algorithms: pqc,
198
+ severity: isPqcReady && pqc.ml_kem ? 'info' : (isPqcReady ? 'medium' : 'high'),
161
199
  skill_hint: 'pqc-first',
162
200
  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).`
201
+ ? (pqc.ml_kem
202
+ ? `OpenSSL ${version} — PQC-capable. Probed: ${pqcDetail}.`
203
+ : `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}.`)
204
+ : `OpenSSL ${version} — below 3.5. Upgrade required for PQC support (ML-KEM, ML-DSA, SLH-DSA, FN-DSA, HQC, LMS, XMSS).`
165
205
  });
166
206
  }
167
207
 
@@ -296,6 +336,153 @@ function safeExecFile(cmd, args) {
296
336
  }
297
337
  }
298
338
 
339
+ /**
340
+ * Probe runtime for PQC algorithm availability across the full
341
+ * emerging-standards landscape. Returns boolean flags per algorithm
342
+ * + a `provider_hint` indicating which surface confirmed each one.
343
+ *
344
+ * --- NIST PQC finalized (FIPS 203/204/205, 2024) ---
345
+ * ml_kem ML-KEM (Kyber) — FIPS 203, key encapsulation
346
+ * ml_dsa ML-DSA (Dilithium) — FIPS 204, signatures
347
+ * slh_dsa SLH-DSA (SPHINCS+) — FIPS 205, stateless hash sigs
348
+ *
349
+ * --- NIST PQC draft / alternate (2025+) ---
350
+ * fn_dsa FN-DSA (Falcon) — FIPS 206 draft, compact lattice sigs
351
+ * hqc HQC — alternate KEM selected March 2025
352
+ *
353
+ * --- NIST PQC Round-4 alternates (still relevant in niche / archival) ---
354
+ * frodo FrodoKEM — conservative lattice KEM
355
+ * ntru NTRU / NTRU-Prime / sNTRU — original lattice KEM family
356
+ * mceliece Classic McEliece — code-based, long-term archival use
357
+ * bike BIKE — code-based KEM, OQS-Provider exposed
358
+ *
359
+ * --- NIST additional signature on-ramp (2023+ Round 2) ---
360
+ * hawk HAWK — NTRU-based lattice signatures
361
+ * mayo MAYO — multivariate
362
+ * sqisign SQIsign — isogeny-based, small signatures
363
+ * cross CROSS — code-based
364
+ * uov UOV / SNOVA — Unbalanced Oil & Vinegar multivariate
365
+ * sdith SDitH — code-based (Syndrome Decoding in the Head)
366
+ * mirath MIRATH — code-based (rank metric)
367
+ * faest FAEST — symmetric-key/AES-based signatures
368
+ * perk PERK — code-based (Permuted Kernel Problem)
369
+ *
370
+ * --- Stateful hash signatures (RFC 8391 / 8554) ---
371
+ * lms LMS — Leighton-Micali Signatures (firmware)
372
+ * xmss XMSS — eXtended Merkle Signature Scheme
373
+ * hss HSS — Hierarchical Signature System
374
+ *
375
+ * --- IETF hybrid / composite sigs (emerging RFC drafts) ---
376
+ * composite_sig Composite Signatures (e.g. RSA+ML-DSA, ECDSA+ML-DSA)
377
+ * composite_kem Composite KEMs (e.g. X25519+ML-KEM)
378
+ */
379
+ function probePqcAlgorithms() {
380
+ const result = {
381
+ // NIST finalized
382
+ ml_kem: false, ml_dsa: false, slh_dsa: false,
383
+ // NIST draft / alternate
384
+ fn_dsa: false, hqc: false,
385
+ // NIST Round-4 / niche
386
+ frodo: false, ntru: false, mceliece: false, bike: false,
387
+ // NIST signature on-ramp (Round 2)
388
+ hawk: false, mayo: false, sqisign: false, cross: false,
389
+ uov: false, sdith: false, mirath: false, faest: false, perk: false,
390
+ // Stateful hash sigs
391
+ lms: false, xmss: false, hss: false,
392
+ // IETF composite / hybrid
393
+ composite_sig: false, composite_kem: false,
394
+ provider_hint: {},
395
+ };
396
+ const crypto = require('crypto');
397
+
398
+ // Pattern table — values are regexes matching common spellings
399
+ // emitted by Node / OpenSSL / OQS-Provider / Bouncy Castle. Tested
400
+ // against algorithm-list output specifically (no English-prose
401
+ // false positives expected; the lists contain only algo names).
402
+ const PATTERNS = {
403
+ // NIST finalized
404
+ ml_kem: /\b(ml-?kem|kyber)\b/i,
405
+ ml_dsa: /\b(ml-?dsa|dilithium)\b/i,
406
+ slh_dsa: /\b(slh-?dsa|sphincs(?:\+|plus)?)\b/i,
407
+ // NIST draft / alternate
408
+ fn_dsa: /\b(fn-?dsa|falcon-?\d{3,4})\b/i,
409
+ hqc: /\bhqc(-?\d+)?\b/i,
410
+ // NIST Round-4 / niche
411
+ frodo: /\bfrodo(-?\d+)?(kem)?\b/i,
412
+ ntru: /\b(s?ntru(-?prime)?(-?\d+)?|hrss\d*|hps\d+)\b/i,
413
+ mceliece: /\b(classic-?)?mceliece(-?\d+)?\b/i,
414
+ bike: /\bbike(-?l?\d+)?\b/i,
415
+ // NIST signature on-ramp (Round 2, 2024+)
416
+ hawk: /\bhawk(-?\d+)?\b/i,
417
+ mayo: /\bmayo(-?\d+)?\b/i,
418
+ sqisign: /\bsqi-?sign(?:hd)?\b/i,
419
+ cross: /\bcross-?(rsdp|rsdpg)?-?(small|fast|balanced)?-?[lns]?\d*\b/i,
420
+ uov: /\b(uov|snova)(-?\d+)?\b/i,
421
+ sdith: /\bsd-?i?t?h(-?gf\d+)?(-?cat\d+)?\b/i,
422
+ mirath: /\bmirath(-?\d+)?\b/i,
423
+ faest: /\bfaest(-em)?(-?\d+)?\b/i,
424
+ perk: /\bperk-?[ls]?-?\d*\b/i,
425
+ // Stateful hash signatures
426
+ lms: /\blms(?!-sha)\b/i,
427
+ xmss: /\bxmss(\^?mt)?\b/i,
428
+ hss: /\bhss(?!-sha)\b/i,
429
+ // Composite / hybrid (IETF drafts)
430
+ 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,
431
+ composite_kem: /\bcomposite-?kem\b|\b(x25519|x448|secp\d+)-?(\+|with)-?(ml-?kem|kyber)\b|\bml-?kem-?(\+|with)-?(x25519|x448)\b/i,
432
+ };
433
+
434
+ function record(algo, source) {
435
+ if (!result[algo]) {
436
+ result[algo] = true;
437
+ result.provider_hint[algo] = source;
438
+ }
439
+ }
440
+
441
+ // Channel 1 — Node's crypto APIs (no shellouts). Node 24+ exposes
442
+ // crypto.kemEncapsulate as an experimental ML-KEM gateway; its
443
+ // presence is itself an ML-KEM availability signal.
444
+ try {
445
+ if (typeof crypto.kemEncapsulate === 'function') {
446
+ record('ml_kem', 'node:crypto.kemEncapsulate');
447
+ }
448
+ const allNames = [
449
+ ...(crypto.getCurves ? crypto.getCurves() : []),
450
+ ...(crypto.getHashes ? crypto.getHashes() : []),
451
+ ...(crypto.getCiphers ? crypto.getCiphers() : []),
452
+ ];
453
+ for (const name of allNames) {
454
+ for (const [algo, re] of Object.entries(PATTERNS)) {
455
+ if (re.test(name)) record(algo, `node:${name}`);
456
+ }
457
+ }
458
+ } catch (_) { /* probe failure → fall through to openssl */ }
459
+
460
+ // Channel 2 — `openssl list` enumerations. KEMs and signature
461
+ // algorithms split across two list commands; OQS-Provider exposes
462
+ // the on-ramp / niche algos when installed.
463
+ const kemList = safeExecFile('openssl', ['list', '-kem-algorithms']);
464
+ if (kemList) {
465
+ const kemAlgos = ['ml_kem', 'hqc', 'frodo', 'ntru', 'mceliece', 'bike', 'composite_kem'];
466
+ for (const algo of kemAlgos) {
467
+ if (PATTERNS[algo].test(kemList)) record(algo, 'openssl:list -kem-algorithms');
468
+ }
469
+ }
470
+ const sigList = safeExecFile('openssl', ['list', '-signature-algorithms']);
471
+ if (sigList) {
472
+ const sigAlgos = [
473
+ 'ml_dsa', 'slh_dsa', 'fn_dsa',
474
+ 'hawk', 'mayo', 'sqisign', 'cross', 'uov', 'sdith', 'mirath', 'faest', 'perk',
475
+ 'lms', 'xmss', 'hss',
476
+ 'composite_sig',
477
+ ];
478
+ for (const algo of sigAlgos) {
479
+ if (PATTERNS[algo].test(sigList)) record(algo, 'openssl:list -signature-algorithms');
480
+ }
481
+ }
482
+
483
+ return result;
484
+ }
485
+
299
486
  function probeTls() {
300
487
  const result = spawnSync('openssl', ['s_client', '-connect', 'google.com:443', '-brief'], {
301
488
  input: '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.9.4",
3
+ "version": "0.10.0",
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:03cec6e7-3ff8-4972-bcd1-769761ab963f",
4
+ "serialNumber": "urn:uuid:7ad6ee8c-6fb3-4eee-b37d-d9b770e964b6",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T03:27:58.018Z",
7
+ "timestamp": "2026-05-12T05:38:55.099Z",
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.4",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.10.0",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.9.4",
19
+ "version": "0.10.0",
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.4",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.10.0",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.9.4"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.10.0"
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,