@blamejs/exceptd-skills 0.13.19 → 0.13.20

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.
@@ -133,7 +133,12 @@ const SPEC = {
133
133
  { field: "control_name", check: (v) => typeof v === "string" && v.length > 0, label: "control_name" },
134
134
  { field: "real_requirement", check: (v) => typeof v === "string" && v.length > 20, label: "real_requirement (>20 chars)" },
135
135
  { field: "theater_test", check: (v) => v && typeof v.claim === "string" && typeof v.test === "string", label: "theater_test{claim,test}" },
136
- { field: "evidence_cves", check: (v) => Array.isArray(v) && v.length > 0, label: "evidence_cves" }
136
+ // evidence_cves is required UNLESS the entry declares forward_looking:true.
137
+ // v0.13.19 used per-entry _gap_skip annotations on 84 framework gaps;
138
+ // v0.13.20 replaces that with a first-class schema field operators can
139
+ // see in the JSON. The check honors forward_looking via the entry
140
+ // parameter — see the SCHEMA_FORWARD_LOOKING block in inspect().
141
+ { field: "evidence_cves", check: (v, entry) => (entry && entry.forward_looking === true) || (Array.isArray(v) && v.length > 0), label: "evidence_cves (or forward_looking:true)" }
137
142
  ],
138
143
  refs: []
139
144
  },
@@ -175,7 +180,11 @@ function inspect(catalogKey) {
175
180
  const skip = e._gap_skip && Array.isArray(e._gap_skip.fields) ? new Set(e._gap_skip.fields) : new Set();
176
181
  for (const r of spec.required_context) {
177
182
  if (skip.has(r.field)) continue;
178
- if (!r.check(e[r.field])) {
183
+ // Pass the entry as the second argument so per-field checks can
184
+ // inspect class-level schema flags (forward_looking, etc.). The
185
+ // legacy check-functions only consumed the value; new ones can
186
+ // opt into entry-aware evaluation.
187
+ if (!r.check(e[r.field], e)) {
179
188
  report.missing_context.push({ id, field: r.field, label: r.label });
180
189
  }
181
190
  }
@@ -329,6 +329,15 @@ function extractPlaybookIds(content) {
329
329
  return { indicators: ind, artifacts: arts };
330
330
  }
331
331
 
332
+ // Canonical-form recursive equality replaces JSON.stringify comparison.
333
+ // Pre-v0.13.20 the comparator was JSON.stringify(before.iocs) !==
334
+ // JSON.stringify(after.iocs) — non-canonical: key order, trailing
335
+ // whitespace, and numeric format differences all flagged as "changed"
336
+ // when the operator made no semantic change. Symptoms were patched
337
+ // twice with skip rules (_auto_imported, _iocs_stub) instead of fixing
338
+ // the comparator. v0.13.20 fixes the root cause.
339
+ const { canonicalEqual } = require("../lib/canonical-eq");
340
+
332
341
  function extractCveIocChanges(beforeStr, afterStr) {
333
342
  const before = safeParse(beforeStr) || {};
334
343
  const after = safeParse(afterStr) || {};
@@ -336,27 +345,16 @@ function extractCveIocChanges(beforeStr, afterStr) {
336
345
  const ids = new Set([...Object.keys(before), ...Object.keys(after)]);
337
346
  for (const id of ids) {
338
347
  if (!/^CVE-\d{4}-\d+/.test(id)) continue;
339
- // v0.13.18: skip bulk-imported entries. Auto-imported rows carry stub
340
- // IoCs by design; their per-entry IoCs are not the operator-curated
341
- // surface the diff-coverage gate is designed to police.
348
+ // v0.13.18 retained skip rule: bulk-imported rows whose IoCs are
349
+ // stub-by-design on both sides pure intake-class events, not
350
+ // operator curation. Removing this would surface every fresh KEV
351
+ // bulk-import as a per-CVE iocs-modified finding.
342
352
  const beforeAuto = !!(before[id] && before[id]._auto_imported);
343
353
  const afterAuto = !!(after[id] && after[id]._auto_imported);
344
354
  if (beforeAuto && afterAuto) continue;
345
- // v0.13.19: also skip operator-curated rows whose IoCs are flagged
346
- // as stubs (`_iocs_stub: true` — generic placeholder added by the
347
- // gap-fix pass when an entry was missing iocs entirely). When an
348
- // operator later curates real IoCs the diff-coverage check fires
349
- // normally because the curation step removes _iocs_stub.
350
- const beforeStub = !!(before[id] && before[id]._iocs_stub);
351
- const afterStub = !!(after[id] && after[id]._iocs_stub);
352
- // Existing entry going stub→curated (afterStub=false, beforeStub=true)
353
- // is what we WANT to flag for review. Existing entry going non-stub→
354
- // stub or stub→stub or absent→stub are all auto-fill events the
355
- // diff-coverage gate should not police.
356
- if (afterStub) continue;
357
- const b = JSON.stringify((before[id] && before[id].iocs) || null);
358
- const a = JSON.stringify((after[id] && after[id].iocs) || null);
359
- if (b !== a) changed.add(id);
355
+ const bIocs = (before[id] && before[id].iocs) || null;
356
+ const aIocs = (after[id] && after[id].iocs) || null;
357
+ if (!canonicalEqual(bIocs, aIocs)) changed.add(id);
360
358
  }
361
359
  return changed;
362
360
  }
@@ -42,6 +42,19 @@ const path = require("path");
42
42
  const ROOT = path.join(__dirname, "..");
43
43
  const TODAY = new Date().toISOString().slice(0, 10);
44
44
 
45
+ // v0.13.20 class-3.11 fix: refreshers read their required-context list
46
+ // from the audit SPEC. Eliminates the parallel hardcoded field arrays
47
+ // that v0.13.17→19 carried (and forgot to keep in sync — the v0.13.19
48
+ // audit found 106 ATT&CK rows missing `description` + `tactic` because
49
+ // the v0.13.18 backfill list omitted those fields). One source of truth
50
+ // = the audit-catalog-gaps SPEC.
51
+ const AUDIT_SPEC = require("./audit-catalog-gaps.js").SPEC;
52
+ function specRequiredFields(catalogKey) {
53
+ const spec = AUDIT_SPEC[catalogKey];
54
+ if (!spec || !Array.isArray(spec.required_context)) return [];
55
+ return spec.required_context.map((r) => r.field);
56
+ }
57
+
45
58
  function fetchUrl(url) {
46
59
  return new Promise((resolve, reject) => {
47
60
  https.get(url, { headers: { "User-Agent": "exceptd-refresh-upstream-catalogs" } }, (r) => {