@blamejs/exceptd-skills 0.18.12 → 0.18.14

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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/exceptd.js +26 -0
  3. package/data/_indexes/_meta.json +2 -2
  4. package/data/playbooks/ai-api.json +2 -2
  5. package/data/playbooks/ai-discovered-cve-triage.json +1 -1
  6. package/data/playbooks/audit-log-integrity.json +3 -3
  7. package/data/playbooks/cicd-pipeline-compromise.json +1 -1
  8. package/data/playbooks/citation-hygiene.json +2 -2
  9. package/data/playbooks/containers.json +2 -2
  10. package/data/playbooks/cred-stores.json +1 -1
  11. package/data/playbooks/crypto-codebase.json +1 -1
  12. package/data/playbooks/crypto.json +1 -1
  13. package/data/playbooks/decompression-dos.json +3 -3
  14. package/data/playbooks/framework.json +1 -1
  15. package/data/playbooks/hardening.json +1 -1
  16. package/data/playbooks/identity-sso-compromise.json +1 -1
  17. package/data/playbooks/kernel.json +2 -2
  18. package/data/playbooks/library-author.json +1 -1
  19. package/data/playbooks/llm-tool-use-exfil.json +1 -1
  20. package/data/playbooks/log-injection-telemetry.json +3 -3
  21. package/data/playbooks/mail-server-hardening.json +3 -3
  22. package/data/playbooks/mcp.json +2 -2
  23. package/data/playbooks/multitenancy-isolation.json +3 -3
  24. package/data/playbooks/network-trust.json +3 -3
  25. package/data/playbooks/post-quantum-migration.json +1 -1
  26. package/data/playbooks/privacy-consent-ops.json +3 -3
  27. package/data/playbooks/runtime.json +2 -2
  28. package/data/playbooks/sbom.json +1 -1
  29. package/data/playbooks/secrets.json +1 -1
  30. package/data/playbooks/self-update-integrity.json +3 -3
  31. package/data/playbooks/supply-chain-recovery.json +2 -2
  32. package/data/playbooks/vc-wallet-trust.json +5 -5
  33. package/data/playbooks/webhook-callback-abuse.json +1 -1
  34. package/lib/auto-discovery.js +10 -1
  35. package/lib/cve-regression-watcher.js +5 -1
  36. package/lib/job-queue.js +9 -2
  37. package/lib/lint-skills.js +34 -5
  38. package/lib/playbook-runner.js +51 -4
  39. package/lib/prefetch.js +15 -7
  40. package/lib/schemas/skill-frontmatter.schema.json +2 -2
  41. package/lib/source-ghsa.js +8 -2
  42. package/lib/source-osv.js +19 -3
  43. package/lib/ttp-mapper.js +15 -0
  44. package/lib/upstream-check.js +17 -3
  45. package/lib/validate-cve-catalog.js +16 -4
  46. package/lib/validate-package.js +9 -2
  47. package/lib/validate-playbooks.js +112 -10
  48. package/lib/validate-vendor.js +11 -5
  49. package/lib/xml-tokenizer.js +36 -8
  50. package/manifest.json +53 -53
  51. package/package.json +1 -1
  52. package/sbom.cdx.json +117 -117
  53. package/sources/validators/cve-validator.js +7 -1
  54. package/sources/validators/rfc-validator.js +21 -3
  55. package/sources/validators/version-pin-validator.js +39 -2
  56. package/vendor/blamejs/_PROVENANCE.json +3 -2
  57. package/vendor/blamejs/worker-pool.js +6 -0
@@ -272,7 +272,13 @@ async function validateCve(cveId, localEntry) {
272
272
  const disputed = (fetched.cve_tags || []).some(t => /disputed/i.test(t)) ||
273
273
  /disputed/i.test(fetched.nvd_vuln_status || '');
274
274
  if (cveFoundInNvd && disputed) {
275
- pushDiscrepancy(discrepancies, 'cve_status', local?.status ?? null, 'disputed', 'high');
275
+ // `local` is the projected shape (cvss_*, cisa_kev*, epss_*) and never
276
+ // carries a status field — reading local?.status here always yielded null,
277
+ // so the local side of a disputed discrepancy reported nothing. Read the
278
+ // catalog's own status/cve_status off the RAW input so an entry that
279
+ // already records the dispute (or a stale "active" status) is surfaced.
280
+ const localCveStatus = localEntry?.cve_status ?? localEntry?.status ?? null;
281
+ pushDiscrepancy(discrepancies, 'cve_status', localCveStatus, 'disputed', 'high');
276
282
  }
277
283
 
278
284
  // --- Compare CVSS (only if NVD reachable & has data) ---
@@ -13,6 +13,10 @@
13
13
  * drift — at least one of those fields disagrees.
14
14
  * missing — Datatracker does not have this RFC / draft.
15
15
  * unreachable — network failed or the request timed out.
16
+ * skipped — the catalog key is not an IETF RFC/DRAFT (e.g. CSAF-2.0,
17
+ * ISO-29147, ISO-30111). Datatracker only tracks IETF
18
+ * documents, so these have no upstream to compare against —
19
+ * they are intentionally not validated here, NOT drift.
16
20
  *
17
21
  * Zero external dependencies. Node 24 stdlib (fetch + AbortController).
18
22
  *
@@ -107,15 +111,26 @@ function compareEntry(local, upstream) {
107
111
  return discrepancies;
108
112
  }
109
113
 
114
+ // Datatracker only tracks IETF RFCs and Internet-Drafts. A catalog key that is
115
+ // neither (CSAF-2.0, ISO-29147, ISO-30111, …) has no IETF upstream to compare
116
+ // against, so it is out of scope for this validator rather than a discrepancy.
117
+ function isIetfKey(id) {
118
+ return typeof id === 'string' && (id.startsWith('RFC-') || id.startsWith('DRAFT-'));
119
+ }
120
+
110
121
  async function validateRfc(id, entry) {
111
122
  let docName;
112
123
  if (id.startsWith('RFC-')) docName = rfcNumberFromKey(id);
113
124
  else if (id.startsWith('DRAFT-')) docName = draftSlugFromKey(id);
114
125
  else {
126
+ // Not an IETF document — there is nothing on Datatracker to diff against.
127
+ // Report a distinct, non-drift status with no discrepancies so callers
128
+ // don't surface a permanent false-positive "drift" for every ISO/CSAF
129
+ // reference the catalog carries.
115
130
  return {
116
131
  id,
117
- status: 'drift',
118
- discrepancies: [`unrecognized catalog key shape: ${id}`],
132
+ status: 'skipped',
133
+ discrepancies: [],
119
134
  local: entry,
120
135
  fetched: null,
121
136
  };
@@ -150,7 +165,10 @@ async function validateRfc(id, entry) {
150
165
  }
151
166
 
152
167
  async function validateAllRfcs(refs, { concurrency = 4 } = {}) {
153
- const ids = Object.keys(refs).filter(k => !k.startsWith('_'));
168
+ // Skip "_"-prefixed meta keys AND any non-IETF reference (ISO/CSAF/etc.):
169
+ // Datatracker has no record of those, so validating them only ever produced
170
+ // permanent false-positive "drift". They are simply out of scope here.
171
+ const ids = Object.keys(refs).filter(k => !k.startsWith('_') && isIetfKey(k));
154
172
  const results = [];
155
173
  for (let i = 0; i < ids.length; i += concurrency) {
156
174
  const batch = ids.slice(i, i + concurrency);
@@ -7,8 +7,17 @@
7
7
  *
8
8
  * - MITRE ATLAS: GitHub releases (mitre-atlas/atlas-data)
9
9
  * - MITRE ATT&CK: GitHub releases (mitre-attack/attack-stix-data)
10
- * - MITRE D3FEND: GitHub releases (mitre-d3fend/d3fend-data)
11
- * - MITRE CWE: GitHub releases (mitre/cwe)
10
+ *
11
+ * MITRE D3FEND and MITRE CWE do NOT publish tagged GitHub releases — D3FEND
12
+ * ships its ontology from d3fend/d3fend-ontology untagged, and CWE distributes
13
+ * XML from cwe.mitre.org. Their pin currency is tracked on the upstream-check
14
+ * path (lib/upstream-check.js) against those canonical mitre.org endpoints, not
15
+ * via the GitHub releases API. They remain enumerated here for completeness but
16
+ * are flagged `github_releases: false`, so the GitHub-release check is skipped
17
+ * for them and they are reported as `tracked_elsewhere` rather than emitting a
18
+ * spurious `unreachable` (a release API call for a repo with no releases always
19
+ * fails). This keeps the live path checking the same two GitHub-release repos
20
+ * the cache path (pinsDiffFromCache) checks.
12
21
  *
13
22
  * Each check returns:
14
23
  * { pin_name, local_version, latest_version, drift: bool, source_url, error? }
@@ -29,24 +38,35 @@ const PINS = [
29
38
  repo: "mitre-atlas/atlas-data",
30
39
  local_path_hint: "manifest.json — atlas_version",
31
40
  strip_v_prefix: true,
41
+ github_releases: true,
32
42
  },
33
43
  {
34
44
  pin_name: "attack_version",
35
45
  repo: "mitre-attack/attack-stix-data",
36
46
  local_path_hint: "manifest.json — attack_version",
37
47
  strip_v_prefix: true,
48
+ github_releases: true,
38
49
  },
39
50
  {
51
+ // D3FEND does not tag GitHub releases — currency is tracked via
52
+ // lib/upstream-check.js against the canonical mitre.org endpoint. Listed
53
+ // here for completeness; the GitHub-release check is skipped.
40
54
  pin_name: "d3fend_version",
41
55
  repo: "d3fend/d3fend-data",
42
56
  local_path_hint: "data/d3fend-catalog.json _meta.version",
43
57
  strip_v_prefix: true,
58
+ github_releases: false,
59
+ tracked_via: "lib/upstream-check.js (d3fend.mitre.org)",
44
60
  },
45
61
  {
62
+ // CWE distributes XML from cwe.mitre.org with no tagged GitHub releases —
63
+ // currency is tracked via lib/upstream-check.js. GitHub-release check skipped.
46
64
  pin_name: "cwe_version",
47
65
  repo: "mitre/cwe",
48
66
  local_path_hint: "data/cwe-catalog.json _meta.version",
49
67
  strip_v_prefix: true,
68
+ github_releases: false,
69
+ tracked_via: "lib/upstream-check.js (cwe.mitre.org)",
50
70
  },
51
71
  ];
52
72
 
@@ -113,6 +133,23 @@ async function checkAllPins(ctx) {
113
133
  const out = [];
114
134
  for (const pin of PINS) {
115
135
  const local = normalize(resolveLocalVersion(pin.pin_name, ctx), pin.strip_v_prefix);
136
+ // Pins whose upstream does not publish GitHub releases (D3FEND, CWE) have
137
+ // no release tag to compare against — calling the releases API would always
138
+ // fail and surface a spurious `unreachable`. Report them as tracked
139
+ // elsewhere (drift:null, no error) so the live path checks exactly the same
140
+ // GitHub-release repos as the cache path.
141
+ if (pin.github_releases === false) {
142
+ out.push({
143
+ pin_name: pin.pin_name,
144
+ local_version: local,
145
+ latest_version: null,
146
+ drift: null,
147
+ tracked_elsewhere: true,
148
+ tracked_via: pin.tracked_via || null,
149
+ local_path_hint: pin.local_path_hint,
150
+ });
151
+ continue;
152
+ }
116
153
  const release = await latestGithubRelease(pin.repo);
117
154
  if (release.error) {
118
155
  out.push({
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "worker-pool.js": {
36
36
  "vendored_path": "vendor/blamejs/worker-pool.js",
37
- "vendored_sha256": "fa9814c2d18db221a2dc552cf2a5047467d87a2590cbc9d64b8a0a340e545b55",
37
+ "vendored_sha256": "948549d82e481f241e5babb6bdddd27bf7148fb8ed1c389b9f2f7ad035e4e56b",
38
38
  "upstream_path": "lib/worker-pool.js",
39
39
  "upstream_sha256_at_pin": "262f99e9cc3d4a8f4eba9ad3e28401e8a1f47f78040afa63c9b17eb998437171",
40
40
  "stripped": [
@@ -50,7 +50,8 @@
50
50
  "worker recycle on uncaught error / timeout / exit"
51
51
  ],
52
52
  "exceptd_deltas": [
53
- "scriptPath validator rejects Windows UNC + extended-path prefixes (\\\\?\\, \\\\.\\, \\\\<server>\\) — defense-in-depth against worker-spawn from network shares on win32 platforms"
53
+ "scriptPath validator rejects Windows UNC + extended-path prefixes (\\\\?\\, \\\\.\\, \\\\<server>\\) — defense-in-depth against worker-spawn from network shares on win32 platforms",
54
+ "_onWorkerExit respawn branch: call _maybeResolveDrain() after _drainQueue() so drain() settles when a worker recycles with an empty queue (the no-respawn branch already did; the respawn branch could hang pending drain waiters)."
54
55
  ]
55
56
  },
56
57
  "codepoint-class.js": {
@@ -322,6 +322,12 @@ function create(scriptPath, opts) {
322
322
  if (!terminated && workerSlots.length < size) {
323
323
  try { _spawnWorker(); } catch (_e) { /* already audited */ }
324
324
  _drainQueue();
325
+ // exceptd delta: also settle pending drain() waiters after a respawn.
326
+ // _drainQueue() only schedules queued tasks; if the queue is already
327
+ // empty (a worker recycled with nothing left to do) the pool is now idle
328
+ // and a caller awaiting drain() must be resolved, exactly as the
329
+ // no-respawn branch below does — otherwise drain() hangs forever.
330
+ _maybeResolveDrain();
325
331
  } else {
326
332
  _maybeResolveDrain();
327
333
  }