@blamejs/exceptd-skills 0.12.13 → 0.12.16

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 (101) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/bin/exceptd.js +522 -27
  3. package/data/_indexes/_meta.json +45 -45
  4. package/data/_indexes/activity-feed.json +4 -4
  5. package/data/_indexes/catalog-summaries.json +29 -29
  6. package/data/_indexes/chains.json +3238 -3210
  7. package/data/_indexes/frequency.json +3 -0
  8. package/data/_indexes/jurisdiction-map.json +5 -3
  9. package/data/_indexes/section-offsets.json +712 -685
  10. package/data/_indexes/theater-fingerprints.json +1 -1
  11. package/data/_indexes/token-budget.json +355 -340
  12. package/data/atlas-ttps.json +144 -129
  13. package/data/attack-techniques.json +319 -76
  14. package/data/cve-catalog.json +516 -476
  15. package/data/cwe-catalog.json +1081 -759
  16. package/data/exploit-availability.json +63 -15
  17. package/data/framework-control-gaps.json +867 -843
  18. package/data/playbooks/ai-api.json +3 -1
  19. package/data/playbooks/containers.json +11 -3
  20. package/data/playbooks/cred-stores.json +3 -1
  21. package/data/playbooks/crypto-codebase.json +11 -11
  22. package/data/playbooks/crypto.json +1 -1
  23. package/data/playbooks/hardening.json +3 -1
  24. package/data/playbooks/kernel.json +3 -1
  25. package/data/playbooks/library-author.json +21 -10
  26. package/data/playbooks/mcp.json +1 -1
  27. package/data/playbooks/runtime.json +3 -1
  28. package/data/playbooks/sbom.json +2 -2
  29. package/data/playbooks/secrets.json +3 -1
  30. package/data/rfc-references.json +276 -276
  31. package/keys/EXPECTED_FINGERPRINT +1 -0
  32. package/lib/auto-discovery.js +57 -35
  33. package/lib/cross-ref-api.js +39 -6
  34. package/lib/cve-curation.js +33 -14
  35. package/lib/lint-skills.js +6 -1
  36. package/lib/playbook-runner.js +742 -78
  37. package/lib/prefetch.js +30 -8
  38. package/lib/refresh-external.js +40 -22
  39. package/lib/refresh-network.js +233 -17
  40. package/lib/scoring.js +191 -18
  41. package/lib/source-ghsa.js +219 -37
  42. package/lib/source-osv.js +381 -122
  43. package/lib/validate-catalog-meta.js +64 -9
  44. package/lib/validate-cve-catalog.js +56 -18
  45. package/lib/validate-indexes.js +88 -37
  46. package/lib/validate-playbooks.js +46 -0
  47. package/lib/verify.js +72 -0
  48. package/manifest-snapshot.json +1 -1
  49. package/manifest-snapshot.sha256 +1 -0
  50. package/manifest.json +73 -73
  51. package/orchestrator/dispatcher.js +21 -1
  52. package/orchestrator/event-bus.js +52 -8
  53. package/orchestrator/index.js +279 -20
  54. package/orchestrator/pipeline.js +63 -2
  55. package/orchestrator/scanner.js +32 -10
  56. package/orchestrator/scheduler.js +150 -17
  57. package/package.json +3 -1
  58. package/sbom.cdx.json +7 -7
  59. package/scripts/check-manifest-snapshot.js +32 -0
  60. package/scripts/check-sbom-currency.js +65 -3
  61. package/scripts/check-test-coverage.js +142 -19
  62. package/scripts/predeploy.js +83 -39
  63. package/scripts/refresh-manifest-snapshot.js +55 -4
  64. package/scripts/validate-vendor-online.js +169 -0
  65. package/scripts/verify-shipped-tarball.js +141 -9
  66. package/skills/ai-attack-surface/skill.md +18 -10
  67. package/skills/ai-c2-detection/skill.md +7 -2
  68. package/skills/ai-risk-management/skill.md +5 -4
  69. package/skills/api-security/skill.md +3 -3
  70. package/skills/attack-surface-pentest/skill.md +5 -5
  71. package/skills/cloud-security/skill.md +1 -1
  72. package/skills/compliance-theater/skill.md +8 -8
  73. package/skills/container-runtime-security/skill.md +1 -1
  74. package/skills/dlp-gap-analysis/skill.md +5 -1
  75. package/skills/email-security-anti-phishing/skill.md +1 -1
  76. package/skills/exploit-scoring/skill.md +18 -18
  77. package/skills/framework-gap-analysis/skill.md +6 -6
  78. package/skills/global-grc/skill.md +3 -2
  79. package/skills/identity-assurance/skill.md +2 -2
  80. package/skills/incident-response-playbook/skill.md +4 -4
  81. package/skills/kernel-lpe-triage/skill.md +21 -2
  82. package/skills/mcp-agent-trust/skill.md +17 -10
  83. package/skills/mlops-security/skill.md +2 -1
  84. package/skills/ot-ics-security/skill.md +1 -1
  85. package/skills/policy-exception-gen/skill.md +3 -3
  86. package/skills/pqc-first/skill.md +1 -1
  87. package/skills/rag-pipeline-security/skill.md +7 -3
  88. package/skills/researcher/skill.md +20 -3
  89. package/skills/sector-energy/skill.md +1 -1
  90. package/skills/sector-federal-government/skill.md +1 -1
  91. package/skills/sector-financial/skill.md +3 -3
  92. package/skills/sector-healthcare/skill.md +2 -2
  93. package/skills/security-maturity-tiers/skill.md +7 -7
  94. package/skills/skill-update-loop/skill.md +19 -3
  95. package/skills/supply-chain-integrity/skill.md +1 -1
  96. package/skills/threat-model-currency/skill.md +11 -11
  97. package/skills/threat-modeling-methodology/skill.md +3 -3
  98. package/skills/webapp-security/skill.md +1 -1
  99. package/skills/zeroday-gap-learn/skill.md +51 -7
  100. package/vendor/blamejs/_PROVENANCE.json +4 -1
  101. package/vendor/blamejs/worker-pool.js +38 -0
@@ -0,0 +1 @@
1
+ SHA256:JX04VjFprM7+3gHJdO0Wi4tTCf1RKI9Roza3XOzAe0Y=
@@ -31,6 +31,31 @@ const fs = require("fs");
31
31
  const path = require("path");
32
32
  const { scoreCustom } = require("./scoring");
33
33
 
34
+ // audit M P1-C: stored rwep_factors must reproduce the stored rwep_score.
35
+ // `buildScoringInputs` is the single source of truth for both — it captures
36
+ // the conservative defaults applied to a freshly-imported KEV draft (CISA
37
+ // only lists vulnerabilities with documented exploitation, so we assume a
38
+ // public PoC exists; reboot defaults to true because most KEV-listed CVEs
39
+ // land in the kernel / hypervisor / vendor firmware where reboot is the
40
+ // norm). The same input object is then handed to scoreCustom for the score
41
+ // AND mapped into the `rwep_factors` shape stored on the draft. Calling
42
+ // scoring.validate() on the post-import catalog will no longer flag every
43
+ // auto-imported draft for divergence > 5.
44
+ function buildScoringInputs(kevEntry /*, nvdPayload */) {
45
+ void kevEntry;
46
+ return {
47
+ cisa_kev: true,
48
+ poc_available: true,
49
+ ai_assisted_weapon: false,
50
+ ai_discovered: false,
51
+ active_exploitation: "suspected",
52
+ blast_radius: 15,
53
+ patch_available: false,
54
+ live_patch_available: false,
55
+ reboot_required: true,
56
+ };
57
+ }
58
+
34
59
  const TODAY = new Date().toISOString().slice(0, 10);
35
60
  const TIMEOUT_MS = 10_000;
36
61
  const USER_AGENT = "exceptd-security/auto-discovery (+https://exceptd.com)";
@@ -110,37 +135,17 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
110
135
  const knownRansomware =
111
136
  String(kevEntry.knownRansomwareCampaignUse || "").toLowerCase() === "known";
112
137
 
113
- // Compute initial RWEP. KEV +25, suspected exploitation +10.
114
- // Unknown PoC/AI flags default to false (conservative we don't
115
- // claim more than we know). Blast radius defaults to 15 (mid-range)
116
- // since we can't infer it from KEV metadata alone.
117
- const rwep_factors = {
118
- cisa_kev: true,
119
- poc_available: null, // unknown curation needed
120
- ai_assisted_weapon: null,
121
- ai_discovered: null,
122
- active_exploitation: "suspected", // KEV listing implies exploitation
123
- blast_radius: 15,
124
- patch_available: null,
125
- live_patch_available: null,
126
- reboot_required: null,
127
- };
128
- // scoreCustom() treats null fields as false, which under-counts the
129
- // score. Pass concrete defaults for unknowns: poc_available=true is
130
- // the conservative assumption for KEV entries (CISA generally only
131
- // adds entries with documented exploitation), and reboot_required=
132
- // true biases toward urgency.
133
- const rwep_score = scoreCustom({
134
- cisa_kev: true,
135
- poc_available: true,
136
- ai_assisted_weapon: false,
137
- ai_discovered: false,
138
- active_exploitation: "suspected",
139
- blast_radius: 15,
140
- patch_available: false,
141
- live_patch_available: false,
142
- reboot_required: true,
143
- });
138
+ // audit M P1-C: stored rwep_factors and computed rwep_score MUST agree.
139
+ // Previously rwep_factors held nulls (for unknown poc/ai/reboot) but
140
+ // rwep_score was computed from concrete defaults (poc=true, reboot=true).
141
+ // `scoring.validate()` then flagged every auto-imported draft for
142
+ // divergence > 5. Now: one canonical input object → both surfaces.
143
+ // The curation flow rewrites these once an operator answers the editorial
144
+ // questions; until then, the boolean shape on rwep_factors is the
145
+ // conservative-default snapshot and reproduces the score exactly.
146
+ const scoringInputs = buildScoringInputs(kevEntry, nvdPayload);
147
+ const rwep_factors = { ...scoringInputs };
148
+ const rwep_score = scoreCustom(scoringInputs);
144
149
 
145
150
  const product = [kevEntry.vendorProject, kevEntry.product]
146
151
  .filter(Boolean)
@@ -184,10 +189,21 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
184
189
  "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
185
190
  kevEntry.notes ? String(kevEntry.notes) : null,
186
191
  ].filter(Boolean),
187
- source_verified: false,
192
+ // v0.12.15 (audit M P1-B): schema requires source_verified to be a
193
+ // YYYY-MM-DD string OR null; the prior `false` boolean produced an
194
+ // entry that failed strict catalog validation. Use null to mean
195
+ // "not yet verified" — operators populate the date during curation.
196
+ source_verified: null,
188
197
  last_updated: TODAY,
189
198
  last_verified: TODAY,
190
- _auto_imported: {
199
+ // v0.12.15 (audit M P1-D): `_auto_imported` must be the boolean `true`
200
+ // for lib/validate-cve-catalog.js's draft-recognition check (strict
201
+ // `=== true` comparison). The prior object-shape was non-recognizable
202
+ // and the strict validator treated KEV-discovered drafts as
203
+ // hard-error entries instead of warning-tier drafts. The provenance
204
+ // metadata that used to be inline now lives in `_auto_imported_meta`.
205
+ _auto_imported: true,
206
+ _auto_imported_meta: {
191
207
  source: "KEV discovery",
192
208
  imported_at: TODAY,
193
209
  curation_needed: [
@@ -476,14 +492,20 @@ async function discoverNewRfcs(ctx, opts = {}) {
476
492
  skills_referencing: [],
477
493
  errata_count: null,
478
494
  last_verified: TODAY,
479
- _auto_imported: {
495
+ // v0.12.15 (audit M P1-D, P3-T): boolean `_auto_imported: true` for
496
+ // strict-validator recognition; provenance moved to sibling
497
+ // `_auto_imported_meta`. Errata-URL hint converted to a real template
498
+ // literal so the rfc number actually interpolates (the previous double-
499
+ // quoted string left `${number}` as literal text in operator output).
500
+ _auto_imported: true,
501
+ _auto_imported_meta: {
480
502
  source: `RFC discovery (IETF ${wg} working group)`,
481
503
  imported_at: TODAY,
482
504
  curation_needed: [
483
505
  "relevance — project-specific framing of how this RFC matters for mid-2026 threats",
484
506
  "lag_notes — what gaps remain or where the RFC falls short",
485
507
  "skills_referencing — list of skills that should cite this RFC",
486
- "errata_count — populate from <rfc-editor.org/errata/rfc${number}>",
508
+ `errata_count — populate from <rfc-editor.org/errata/rfc${number}>`,
487
509
  ],
488
510
  },
489
511
  };
@@ -21,6 +21,15 @@ const INDEX_DIR = path.join(DATA_DIR, '_indexes');
21
21
 
22
22
  const _cache = new Map();
23
23
 
24
+ // v0.12.14 (audit C-F7): catalog corruption no longer crashes the runner
25
+ // uncaught. A malformed JSON file in data/ used to produce a SyntaxError
26
+ // at require-time of any consumer (lib/playbook-runner.js), which threw
27
+ // out of the run() entrypoint without honoring AGENTS.md's "non-zero
28
+ // exit + {ok:false, error} to stderr" contract. Now: caught + degraded
29
+ // to an empty catalog with a recorded _loadError that downstream code
30
+ // can inspect.
31
+ const _loadErrors = [];
32
+
24
33
  function loadCatalog(filename) {
25
34
  if (_cache.has(filename)) return _cache.get(filename);
26
35
  const full = path.join(DATA_DIR, filename);
@@ -28,9 +37,17 @@ function loadCatalog(filename) {
28
37
  _cache.set(filename, {});
29
38
  return {};
30
39
  }
31
- const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
32
- _cache.set(filename, parsed);
33
- return parsed;
40
+ try {
41
+ const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
42
+ _cache.set(filename, parsed);
43
+ return parsed;
44
+ } catch (e) {
45
+ _loadErrors.push({ kind: 'catalog', file: filename, error: e.message });
46
+ const stub = {};
47
+ Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
48
+ _cache.set(filename, stub);
49
+ return stub;
50
+ }
34
51
  }
35
52
 
36
53
  function loadIndex(filename) {
@@ -40,9 +57,21 @@ function loadIndex(filename) {
40
57
  _cache.set('idx:' + filename, {});
41
58
  return {};
42
59
  }
43
- const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
44
- _cache.set('idx:' + filename, parsed);
45
- return parsed;
60
+ try {
61
+ const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
62
+ _cache.set('idx:' + filename, parsed);
63
+ return parsed;
64
+ } catch (e) {
65
+ _loadErrors.push({ kind: 'index', file: filename, error: e.message });
66
+ const stub = {};
67
+ Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
68
+ _cache.set('idx:' + filename, stub);
69
+ return stub;
70
+ }
71
+ }
72
+
73
+ function getLoadErrors() {
74
+ return _loadErrors.slice();
46
75
  }
47
76
 
48
77
  function entries(catalog) {
@@ -221,4 +250,8 @@ module.exports = {
221
250
  // Lower-level access (engine uses these directly)
222
251
  _loadCatalog: loadCatalog,
223
252
  _loadIndex: loadIndex,
253
+ // v0.12.14: surface accumulated catalog/index load errors. Returns
254
+ // [{kind, file, error}, ...] for every catalog/index whose JSON
255
+ // parse failed. Empty array on a healthy install.
256
+ getLoadErrors,
224
257
  };
@@ -43,6 +43,14 @@ const path = require("path");
43
43
  // before deciding promotion.
44
44
  const { withCatalogLock } = require("./refresh-external");
45
45
  const { validate: validateAgainstSchema } = require("./validate-cve-catalog");
46
+ // audit J F3: derive rwep_score via the canonical scoring helper rather
47
+ // than a blind `Object.values(...).reduce(sum)`. The helper detects shape
48
+ // (boolean inputs → scoreCustom; post-weight numeric inputs → sum + clamp)
49
+ // so the curation apply-path produces a score that matches whatever the
50
+ // catalog scorer or playbook-runner would have produced for the same
51
+ // factors. Direct dependency on scoring.js is intentional — scoring.js is
52
+ // the authoritative formula.
53
+ const { deriveRwepFromFactors } = require("./scoring");
46
54
 
47
55
  const ROOT = path.resolve(__dirname, "..");
48
56
  const CVE_SCHEMA_PATH = path.join(ROOT, "lib", "schemas", "cve-catalog.schema.json");
@@ -50,11 +58,18 @@ let _cveSchemaCache = null;
50
58
  function loadCveEntrySchema() {
51
59
  if (_cveSchemaCache) return _cveSchemaCache;
52
60
  try {
61
+ // v0.12.15 (audit M P1-A): the prior version of this function looked for
62
+ // either `root.patternProperties["^CVE-\\d{4}-\\d+$"]` or an object
63
+ // `root.additionalProperties`. The actual schema at lib/schemas/cve-
64
+ // catalog.schema.json has NEITHER — its top level IS the entry shape
65
+ // (`{type:'object', required:[...], properties: {...}}`) because
66
+ // validate-cve-catalog.js iterates each CVE id key manually and runs
67
+ // the schema validator over each value. Result: loadCveEntrySchema()
68
+ // always returned null, the v0.12.12 codex P1 #1 fix (strict-schema
69
+ // gating of promotion) was silently disabled, and schema-violating
70
+ // entries promoted anyway. Use the root schema directly.
53
71
  const root = JSON.parse(fs.readFileSync(CVE_SCHEMA_PATH, "utf8"));
54
- const entrySchema =
55
- (root.patternProperties && root.patternProperties["^CVE-\\d{4}-\\d+$"]) ||
56
- (root.additionalProperties && typeof root.additionalProperties === "object" ? root.additionalProperties : null);
57
- _cveSchemaCache = entrySchema || null;
72
+ _cveSchemaCache = root || null;
58
73
  return _cveSchemaCache;
59
74
  } catch {
60
75
  return null;
@@ -264,7 +279,13 @@ function buildQuestionnaire(cveId, draft) {
264
279
  // Pull candidate catalogs. Each is optional — missing catalogs are skipped
265
280
  // gracefully. J7 makes these one-shot loads per process.
266
281
  const atlas = loadJson("data/atlas-ttps.json");
267
- const attack = loadJson("data/attack-ttps.json");
282
+ // v0.12.15 (audit M P1-E): the catalog ships as data/attack-techniques.json
283
+ // (renamed from data/attack-ttps.json before the v0.12.12 release; the
284
+ // canonical file path is also what lib/validate-cve-catalog.js consumes).
285
+ // The prior `data/attack-ttps.json` lookup silently fell back to an empty
286
+ // object via loadJsonRaw's ENOENT handling, so the ATT&CK candidate
287
+ // questionnaire branch always returned zero proposals.
288
+ const attack = loadJson("data/attack-techniques.json");
268
289
  const cwe = loadJson("data/cwe-catalog.json");
269
290
  const frameworkGaps = loadJson("data/framework-control-gaps.json");
270
291
 
@@ -549,17 +570,15 @@ function applyAnswersUnderLock(cveId, catalog, catalogPath, answers) {
549
570
  appliedFields.push(field);
550
571
  }
551
572
 
552
- // Derive rwep_score from rwep_factors when factors supplied without an
553
- // explicit score. lib/scoring.js owns the canonical formula; we sum the
554
- // numeric values here as a fallback.
573
+ // audit J F3: derive rwep_score via the canonical scoring helper rather
574
+ // than a blind sum. deriveRwepFromFactors detects shape (boolean inputs
575
+ // → scoreCustom; post-weight numeric inputs sum + clamp) and routes
576
+ // accordingly, so the apply-path produces a score that agrees with
577
+ // scoring.validate() instead of diverging from it.
555
578
  if ("rwep_factors" in answers && !("rwep_score" in answers)
556
579
  && entry.rwep_factors && typeof entry.rwep_factors === "object") {
557
- let sum = 0;
558
- for (const v of Object.values(entry.rwep_factors)) {
559
- if (typeof v === "number") sum += v;
560
- }
561
- entry.rwep_score = Math.max(0, Math.min(100, sum));
562
- appliedFields.push("rwep_score (derived from rwep_factors)");
580
+ entry.rwep_score = deriveRwepFromFactors(entry.rwep_factors);
581
+ appliedFields.push("rwep_score (derived from rwep_factors via scoring.deriveRwepFromFactors)");
563
582
  }
564
583
 
565
584
  // last_updated reflects the apply moment.
@@ -635,8 +635,13 @@ function loadContext() {
635
635
  */
636
636
  function findOrphanSkillFiles(manifestSkills) {
637
637
  if (!fs.existsSync(SKILLS_DIR)) return [];
638
+ // F19 — manifest paths are stored as forward-slash strings by contract
639
+ // (lib/verify.js validateSkillPath() rejects backslashes). The previous
640
+ // path.sep split was a no-op on Linux and incorrect on Windows when
641
+ // mixed separators arrived through other ingest paths; the cleaner
642
+ // contract is to normalise the comparison key directly.
638
643
  const referenced = new Set(
639
- manifestSkills.map((s) => s.path.split(path.sep).join('/')),
644
+ manifestSkills.map((s) => String(s.path).replace(/\\/g, '/')),
640
645
  );
641
646
  const orphans = [];
642
647
  for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {