@blamejs/exceptd-skills 0.16.21 → 0.16.23

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.
Files changed (47) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +28 -0
  3. package/CONTEXT.md +9 -9
  4. package/README.md +3 -3
  5. package/agents/skill-updater.md +1 -1
  6. package/agents/source-validator.md +3 -4
  7. package/agents/threat-researcher.md +1 -1
  8. package/bin/exceptd.js +19 -7
  9. package/data/_indexes/_meta.json +10 -10
  10. package/data/_indexes/activity-feed.json +12 -12
  11. package/data/_indexes/chains.json +70084 -3852
  12. package/data/_indexes/frequency.json +492 -163
  13. package/data/_indexes/section-offsets.json +16 -16
  14. package/data/_indexes/summary-cards.json +272 -106
  15. package/data/_indexes/token-budget.json +10 -10
  16. package/data/_indexes/trigger-table.json +15 -6
  17. package/data/_indexes/xref.json +218 -26
  18. package/data/cve-catalog.json +10 -10
  19. package/data/cwe-catalog.json +1 -0
  20. package/lib/auto-discovery.js +39 -1
  21. package/lib/collectors/scan-excludes.js +4 -1
  22. package/lib/cve-cli.js +9 -1
  23. package/lib/cve-curation.js +8 -1
  24. package/lib/exit-codes.js +2 -0
  25. package/lib/flag-suggest.js +1 -1
  26. package/lib/lint-skills.js +70 -0
  27. package/lib/playbook-runner.js +59 -11
  28. package/lib/prefetch.js +24 -1
  29. package/lib/refresh-external.js +56 -5
  30. package/lib/rfc-cli.js +8 -1
  31. package/lib/scoring.js +36 -8
  32. package/lib/validate-cve-catalog.js +36 -14
  33. package/lib/validate-package.js +8 -0
  34. package/lib/validate-playbooks.js +42 -0
  35. package/lib/verify.js +4 -3
  36. package/manifest-snapshot.json +4 -2
  37. package/manifest-snapshot.sha256 +1 -1
  38. package/manifest.json +57 -54
  39. package/orchestrator/index.js +48 -4
  40. package/orchestrator/scanner.js +53 -5
  41. package/package.json +1 -1
  42. package/sbom.cdx.json +80 -80
  43. package/scripts/build-indexes.js +42 -8
  44. package/scripts/check-sbom-currency.js +72 -0
  45. package/scripts/release.js +22 -15
  46. package/skills/exploit-scoring/skill.md +8 -8
  47. package/sources/validators/cve-validator.js +6 -1
@@ -31,6 +31,39 @@ function resolveRoot(argv) {
31
31
  return path.join(__dirname, "..");
32
32
  }
33
33
 
34
+ // Entry count for a data/*.json catalog: keys minus the _meta sentinel. The
35
+ // catalogs are objects keyed by entry id (CVE-…, CWE-…, T…, AML.T…, D3-…,
36
+ // RFC-…) with a single _meta block, so the live entry total is the key count
37
+ // excluding _meta.
38
+ function catalogEntryCount(dataDir, file) {
39
+ const p = path.join(dataDir, file);
40
+ // A --root pointed at a partial tree (no such catalog file) skips that
41
+ // token's check rather than crashing — catalog PRESENCE is asserted by
42
+ // the cardinality check above and the per-component hash check below,
43
+ // not by the description parser.
44
+ if (!fs.existsSync(p)) return null;
45
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
46
+ if (Array.isArray(j)) return j.length;
47
+ if (j && typeof j === "object") {
48
+ return Object.keys(j).filter((k) => k !== "_meta").length;
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ // The description string embeds per-catalog ENTRY counts as free text, e.g.
54
+ // "11 catalogs (439 CVEs / 177 CWEs / 805 ATT&CK + ICS / 170 ATLAS /
55
+ // 468 D3FEND / 8888 RFCs)". Each token maps to one data/*.json catalog whose
56
+ // live entry count must match. `label` is the regex-escaped text that follows
57
+ // the number in the description.
58
+ const DESCRIPTION_ENTRY_TOKENS = [
59
+ { file: "cve-catalog.json", label: "CVEs" },
60
+ { file: "cwe-catalog.json", label: "CWEs" },
61
+ { file: "attack-techniques.json", label: "ATT&CK \\+ ICS" },
62
+ { file: "atlas-ttps.json", label: "ATLAS" },
63
+ { file: "d3fend-catalog.json", label: "D3FEND" },
64
+ { file: "rfc-references.json", label: "RFCs" },
65
+ ];
66
+
34
67
  function checkSbomCurrency(root) {
35
68
  const sbomPath = path.join(root, "sbom.cdx.json");
36
69
  const manifestPath = path.join(root, "manifest.json");
@@ -64,6 +97,45 @@ function checkSbomCurrency(root) {
64
97
  errors.push("SBOM is not CycloneDX 1.6");
65
98
  }
66
99
 
100
+ // The SBOM ships per-catalog entry counts and a skill count embedded as free
101
+ // text in metadata.component.description (propagated verbatim from
102
+ // package.json). The numeric properties above only cover catalog/skill
103
+ // CARDINALITY (file count + skill count), so a catalog's entry total can
104
+ // drift past the count baked into the description while the dedicated SBOM
105
+ // gate still passes. Parse each token out of the description and assert it
106
+ // against the live entry count so a stale published-SBOM description fails
107
+ // the gate.
108
+ const description =
109
+ (sbom.metadata && sbom.metadata.component && sbom.metadata.component.description) || "";
110
+ for (const { file, label } of DESCRIPTION_ENTRY_TOKENS) {
111
+ const live = catalogEntryCount(dataDir, file);
112
+ if (live === null) continue;
113
+ const m = description.match(new RegExp("(\\d+)\\s+" + label + "\\b"));
114
+ if (!m) {
115
+ errors.push(
116
+ `SBOM description is missing the "${file.replace(/\.json$/, "")}" entry-count token (${label}) — regenerate via \`npm run refresh-sbom\``
117
+ );
118
+ continue;
119
+ }
120
+ const stated = Number(m[1]);
121
+ if (stated !== live) {
122
+ errors.push(
123
+ `SBOM description entry count for ${label} is ${stated} but live ${file} has ${live} — description is stale; update package.json.description and \`npm run refresh-sbom\``
124
+ );
125
+ }
126
+ }
127
+ // The skill count is embedded in the same description string ("N skills").
128
+ const skillMatch = description.match(/(\d+)\s+skills\b/);
129
+ if (!skillMatch) {
130
+ errors.push(
131
+ "SBOM description is missing the skill-count token (N skills) — regenerate via `npm run refresh-sbom`"
132
+ );
133
+ } else if (Number(skillMatch[1]) !== liveSkills) {
134
+ errors.push(
135
+ `SBOM description skill count is ${Number(skillMatch[1])} but live manifest has ${liveSkills} skills — description is stale; update package.json.description and \`npm run refresh-sbom\``
136
+ );
137
+ }
138
+
67
139
  // component-level cross-check. A renamed or version-bumped
68
140
  // skill that never made it into the SBOM refresh will pass the count
69
141
  // check (the cardinality is unchanged) but the per-component name +
@@ -503,21 +503,27 @@ function cmdRelease() {
503
503
  if (runId) {
504
504
  _run("gh", ["run", "watch", runId, "--exit-status"], { allowFail: true });
505
505
  var concl = _capture("gh", ["run", "view", runId, "--json", "conclusion", "--jq", ".conclusion"]).stdout;
506
+ // A non-success conclusion is a hard failure: the publish either failed or
507
+ // is unconfirmable, and either way the release is not done. Warning-and-
508
+ // continuing let a stalled publish read as a clean release.
506
509
  if (concl !== "success") {
507
- console.error("warning: release.yml conclusion=" + concl + " — re-check before trusting npm");
508
- } else {
509
- _ok("release.yml: success");
510
+ throw new Error("release: release.yml conclusion=" + (concl || "(unknown)") +
511
+ " the publish workflow did not finish successfully; re-check release.yml before treating the release as done");
510
512
  }
513
+ _ok("release.yml: success");
511
514
  } else {
512
- console.log("no release.yml run found yet (tag push may still be propagating)");
515
+ throw new Error("release: no release.yml run found for the tag the publish workflow has not started; " +
516
+ "confirm the tag was pushed and the workflow fired before treating the release as done");
513
517
  }
514
518
 
515
519
  _section("verify npm");
516
520
  var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
517
521
  console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") + " (expected " + next + ")");
522
+ // Require a POSITIVE confirmation: the queried npm version must equal `next`.
523
+ // The hard failure is asserted at the end of the phase (after the tarball
524
+ // verify). An empty stdout (registry/auth/network failure) is treated as a
525
+ // mismatch — an unconfirmable publish is a failure, not a success.
518
526
  if (npmVersion === next) _ok("npm matches " + next);
519
- // A mismatch is asserted as a hard failure at the end of the phase (after
520
- // the tarball verify), so a stalled publish can't read as a clean release.
521
527
 
522
528
  _section("fresh-tarball signature verify");
523
529
  // Verify against the EXACT bytes a downstream consumer installs — the
@@ -535,18 +541,19 @@ function cmdRelease() {
535
541
  throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
536
542
  }
537
543
 
538
- // An npm-version mismatch after the workflow finished is not mere
539
- // propagation lag fail so a stalled/failed publish can't read as a
540
- // completed release. (A genuinely in-flight publish is caught by the
541
- // workflow-conclusion check above; by the time we query npm post-watch the
542
- // version should be live.)
543
- if (npmVersion && npmVersion !== next) {
544
- throw new Error("release: npm shows " + npmVersion + " but expected " + next +
545
- " publish did not complete; re-check release.yml before treating the release as done");
544
+ // Require a positive npm confirmation after the workflow finished. A version
545
+ // that is empty (query failed) OR != next is not mere propagation lag — fail
546
+ // so a stalled/failed/unconfirmable publish can't read as a completed
547
+ // release. (A genuinely in-flight publish is caught by the workflow-
548
+ // conclusion check above; by the time we query npm post-watch the version
549
+ // should be live.) The message reports the value actually queried.
550
+ if (npmVersion !== next) {
551
+ throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
552
+ " — publish did not complete or could not be confirmed; re-check release.yml before treating the release as done");
546
553
  }
547
554
 
548
555
  console.log("\nThe landing site auto-injects the version from jsDelivr @latest — no manual deploy.");
549
- console.log("Release complete: npm shows " + next + " and the shipped tarball verifies.");
556
+ console.log("Release complete: npm shows " + npmVersion + " and the shipped tarball verifies.");
550
557
  }
551
558
 
552
559
  function cmdAll(opts) {
@@ -109,7 +109,7 @@ RWEP = min(100, max(0,
109
109
  (poc_public × 20) +
110
110
  (ai_assisted × 15) +
111
111
  (active_expl × 20) +
112
- (blast_radius × 15) -
112
+ (blast_radius × 30) -
113
113
  (patch_avail × 15) -
114
114
  (live_patch × 10) +
115
115
  (reboot_req × 5)
@@ -135,11 +135,11 @@ RWEP = min(100, max(0,
135
135
  - Score contribution: +20 points if confirmed
136
136
  - Rationale: Confirmed exploitation means the threat is not theoretical. Treat as an incident-level response trigger.
137
137
 
138
- **blast_radius** (0.0 to 1.0 scaled to 0–15): How broad is the affected population?
139
- - 15 points: Affects all Linux systems since a specific kernel version (e.g., Copy Fail: all 4.14+)
140
- - 10 points: Affects a major distribution's default configuration
141
- - 7 points: Affects a specific distribution or configuration
142
- - 3 points: Affects a narrow software version range
138
+ **blast_radius** (0.0 to 1.0 scaled to 0–30): How broad is the affected population?
139
+ - 30 points: Affects all Linux systems since a specific kernel version (e.g., Copy Fail: all 4.14+)
140
+ - 20 points: Affects a major distribution's default configuration
141
+ - 14 points: Affects a specific distribution or configuration
142
+ - 6 points: Affects a narrow software version range
143
143
  - 0 points: Affects only highly specific configurations
144
144
 
145
145
  **patch_avail** (0 or 1): Is a patch available?
@@ -290,7 +290,7 @@ For a CVE not in the pre-calculated catalog, collect:
290
290
 
291
291
  ### Step 2: Apply RWEP formula
292
292
 
293
- Calculate factor values (binary 0/1 or scaled 0–1 for blast radius) and apply formula.
293
+ Calculate factor values (binary 0/1, or scaled 0–30 for blast radius) and apply formula.
294
294
 
295
295
  ### Step 3: Generate remediation timeline
296
296
 
@@ -336,7 +336,7 @@ The skill produces a per-CVE Exploit Priority Assessment showing the RWEP score,
336
336
  | PoC Public | Yes/No | +20/0 |
337
337
  | AI-Assisted | Yes/No | +15/0 |
338
338
  | Active Exploitation | Confirmed/Suspected/No | +20/+10/0 |
339
- | Blast Radius | [description] | [0-15] |
339
+ | Blast Radius | [description] | [0-30] |
340
340
  | Patch Available | Yes/No | -15/0 |
341
341
  | Live Patch Available | Yes/No | -10/0 |
342
342
  | Reboot Required | Yes/No | +5/0 |
@@ -286,7 +286,12 @@ async function validateCve(cveId, localEntry) {
286
286
  if (typeof local.cisa_kev === 'boolean' && local.cisa_kev !== fetched.in_kev) {
287
287
  pushDiscrepancy(discrepancies, 'cisa_kev', local.cisa_kev, fetched.in_kev, 'high');
288
288
  }
289
- if (local.cisa_kev_date && fetched.kev_date && local.cisa_kev_date !== fetched.kev_date) {
289
+ // Emit whenever upstream carries a listing date the local entry lacks or
290
+ // contradicts — a first listing arrives as cisa_kev_date:null locally, and
291
+ // requiring a truthy local date here meant the flag flipped without its
292
+ // date, which the strict catalog validator rejects (KEV-listed entries
293
+ // must carry their listing date).
294
+ if (fetched.kev_date && local.cisa_kev_date !== fetched.kev_date) {
290
295
  pushDiscrepancy(discrepancies, 'cisa_kev_date', local.cisa_kev_date, fetched.kev_date, 'low');
291
296
  }
292
297
  }