@blamejs/exceptd-skills 0.19.32 → 0.19.34

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 (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,38 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-upstream-catalogs.js
5
- *
6
- * Unified entrypoint + library for refreshing the four canonical upstream
7
- * catalogs from their official sources. Each refresher is idempotent and
8
- * never overwrites operator-curated entries (rows that lack the
9
- * `_auto_imported: true` flag are preserved verbatim).
10
- *
11
- * rfc ietf-rfc-index data/rfc-references.json
12
- * attack mitre-attack-stix data/attack-techniques.json
13
- * atlas mitre-atlas-stix data/atlas-ttps.json
14
- * d3fend mitre-d3fend-owl data/d3fend-catalog.json
15
- *
16
- * CLI usage:
17
- *
18
- * node scripts/refresh-upstream-catalogs.js # all four
19
- * node scripts/refresh-upstream-catalogs.js --source rfc # one
20
- * node scripts/refresh-upstream-catalogs.js --source rfc,atlas # two
21
- * node scripts/refresh-upstream-catalogs.js --dry-run # report only
22
- * CAP=200 node scripts/refresh-upstream-catalogs.js --source attack
23
- *
24
- * Module usage (per-type wrappers under scripts/refresh-{rfc,attack,atlas,
25
- * d3fend}.js import the corresponding refreshX function from this file):
26
- *
27
- * const { refreshRfc } = require("./refresh-upstream-catalogs.js");
28
- * await refreshRfc({ dry: false });
29
- *
30
- * npm aliases (package.json scripts):
31
- * refresh-upstream-catalogs runs all four
32
- * refresh-rfc-index --source rfc
33
- * refresh-mitre-attack --source attack
34
- * refresh-mitre-atlas --source atlas
35
- * refresh-mitre-d3fend --source d3fend
4
+ * Refreshes the upstream catalogs (IETF RFC index, MITRE ATT&CK, ICS-ATT&CK,
5
+ * ATLAS, D3FEND) from their official sources. Refreshers are idempotent and
6
+ * additive: a populated field is never overwritten, and only `_auto_imported`
7
+ * rows take a status bump.
36
8
  */
37
9
 
38
10
  const fs = require("fs");
@@ -42,12 +14,8 @@ const path = require("path");
42
14
  const ROOT = path.join(__dirname, "..");
43
15
  const TODAY = new Date().toISOString().slice(0, 10);
44
16
 
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.
17
+ // Required-context field names come from the audit-catalog-gaps SPEC, never a
18
+ // second hardcoded list that would drift from it.
51
19
  const AUDIT_SPEC = require("./audit-catalog-gaps.js").SPEC;
52
20
  function specRequiredFields(catalogKey) {
53
21
  const spec = AUDIT_SPEC[catalogKey];
@@ -57,15 +25,9 @@ function specRequiredFields(catalogKey) {
57
25
 
58
26
  const MAX_REDIRECTS = 5;
59
27
 
60
- // Hardened fetch helper. Three properties the hand-rolled follower lacked:
61
- // 1. Redirect-depth cap + base-URL resolution + response drain, so a
62
- // redirect loop rejects within the cap (rather than recursing/hanging
63
- // unbounded) and a relative Location resolves against the current URL.
64
- // 2. 4xx/5xx (and a missing statusCode edge) reject instead of resolving an
65
- // error body as a "successful" empty result — every consumer fails
66
- // closed on an HTTP error rather than stamping _meta on a non-fetch.
67
- // 3. A 3xx with no Location header rejects with a clear message rather than
68
- // throwing an opaque ERR_INVALID_URL on `new URL(undefined, url)`.
28
+ // Rejects on anything but a 2xx, so an error body can never reach a consumer as
29
+ // a "successful" empty result and get _meta stamped on a non-fetch. A redirect
30
+ // resolves Location against the current URL and rejects past MAX_REDIRECTS.
69
31
  function fetchUrl(url, depth = 0) {
70
32
  return new Promise((resolve, reject) => {
71
33
  https.get(url, { headers: { "User-Agent": "exceptd-refresh-upstream-catalogs" } }, (r) => {
@@ -99,11 +61,8 @@ function loadCatalog(rel) {
99
61
  return JSON.parse(fs.readFileSync(path.join(ROOT, "data", rel), "utf8"));
100
62
  }
101
63
 
102
- // Atomic write: a crash / disk-full / SIGKILL mid-write would otherwise leave a
103
- // truncated JSON catalog on disk. Write to a temp sibling and rename — rename is
104
- // atomic on POSIX and on same-volume Windows renames (the .tmp sibling is
105
- // adjacent to the target, same volume), so a reader / the next run only ever
106
- // sees the complete old or complete new file. Mirrors build-indexes#writeJson.
64
+ // Temp sibling + atomic rename, so a crash mid-write cannot leave a truncated
65
+ // catalog behind. Mirrors build-indexes#writeJson.
107
66
  function writeCatalog(rel, obj) {
108
67
  const abs = path.join(ROOT, "data", rel);
109
68
  const tmp = `${abs}.tmp-${process.pid}`;
@@ -117,8 +76,6 @@ function getTag(blk, tag) {
117
76
  return m ? m[1].trim() : null;
118
77
  }
119
78
 
120
- // ---------------- RFC ----------------
121
-
122
79
  const RFC_SRC = "https://www.rfc-editor.org/rfc-index.xml";
123
80
  const RFC_STATUS_MAP = {
124
81
  "INTERNET STANDARD": "Internet Standard",
@@ -136,8 +93,7 @@ const RFC_MONTHS = {
136
93
  September: "09", October: "10", November: "11", December: "12"
137
94
  };
138
95
 
139
- // Extract every doc-id reference inside a parent tag like
140
- // <obsoleted-by><doc-id>RFC123</doc-id><doc-id>RFC456</doc-id></obsoleted-by>.
96
+ // Every <doc-id> inside a parent tag: <obsoleted-by><doc-id>RFC123</doc-id>…
141
97
  function getDocIdList(blk, parentTag) {
142
98
  const re = new RegExp(`<${parentTag}>([\\s\\S]*?)<\\/${parentTag}>`);
143
99
  const m = blk.match(re);
@@ -166,7 +122,6 @@ function getAbstract(blk) {
166
122
  return paras.length ? paras.join(" ") : null;
167
123
  }
168
124
 
169
- // <keywords><kw>k1</kw><kw>k2</kw></keywords>
170
125
  function getKeywords(blk) {
171
126
  const m = blk.match(/<keywords>([\s\S]*?)<\/keywords>/);
172
127
  if (!m) return [];
@@ -211,10 +166,6 @@ function parseRfcEntry(blk) {
211
166
  const published = year && month ? `${year}-${month}` : (year || "unknown");
212
167
  const hasErrata = /<errata-url>/.test(blk);
213
168
  const obsoleted = /<obsoleted-by>/.test(blk);
214
- // New context-search fields (v0.13.18+): the AI needs more than the
215
- // title to locate an RFC by topic. abstract + keywords + area +
216
- // wg_acronym + stream + authors + obsoletes/updates relationships are
217
- // all present in the IETF index — we were only extracting title before.
218
169
  const abstract = getAbstract(blk);
219
170
  const keywords = getKeywords(blk);
220
171
  const area = getTag(blk, "area");
@@ -253,12 +204,8 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
253
204
  if (e.obsoleted || e.status === "HISTORIC" || e.status === "UNKNOWN") continue;
254
205
  entries.push(e);
255
206
  }
256
- // Sanity floor: the IETF index has ~9000+ RFCs, so a successful fetch can
257
- // never parse to zero entries. A zero count means the fetch returned an
258
- // error/empty/soft-error body (a 200 with a CDN error page, a captive portal,
259
- // or a truncated body the HTTP-status guard can't see) — refuse to stamp
260
- // _meta or write rfc-references.json, matching the JSON-parse failures the
261
- // STIX sources surface for free (an empty index is never a legitimate result).
207
+ // A successful fetch of a ~9000-RFC index can never parse to zero. Zero means a
208
+ // soft-error body — a CDN error page under a 200 — the status guard cannot see.
262
209
  if (backfillable.length === 0) {
263
210
  throw new Error("RFC index parsed 0 entries (fetch likely returned an error/empty body) — refusing to stamp _meta or write rfc-references.json");
264
211
  }
@@ -266,19 +213,16 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
266
213
  const cat = _loadCatalog("rfc-references.json");
267
214
  const existing = new Set(Object.keys(cat).filter((k) => k !== "_meta"));
268
215
  let added = 0, statusBumped = 0, backfilledCount = 0;
269
- // First pass: backfill ALL existing rows from the broader entry set
270
- // (including obsoleted historics). Operator may have curated an
271
- // obsoleted RFC in for documentation; we still want abstract/authors.
216
+ // Backfill every existing row from the broader entry set, obsoleted historics
217
+ // included: a curated obsoleted RFC still wants its abstract and authors.
272
218
  for (const e of backfillable) {
273
219
  const id = `RFC-${e.num}`;
274
220
  if (!existing.has(id)) continue;
275
221
  const cur = cat[id];
276
222
  if (!cur) continue;
277
223
  let touched = false;
278
- // Mirror the new-add path's `|| e.status` fallback: an upstream status
279
- // outside RFC_STATUS_MAP must NOT write `undefined` (which would drop the
280
- // status field on an existing curated row). Fall back to the raw upstream
281
- // status, and only bump when the mapped value actually differs.
224
+ // An upstream status outside RFC_STATUS_MAP falls back to the raw status
225
+ // rather than writing `undefined` and dropping the field on an existing row.
282
226
  const mapped = RFC_STATUS_MAP[e.status] || e.status;
283
227
  if (cur._auto_imported && mapped && cur.status !== mapped) {
284
228
  cur.status = mapped;
@@ -302,13 +246,9 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
302
246
  if (!cur.html_url) { cur.html_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.html`; touched = true; }
303
247
  if (touched) { cur.last_verified = TODAY; backfilledCount++; }
304
248
  }
305
- // Second pass: add new "current" entries that weren't in the catalog.
306
- // Add new rows from the FULL index, not just the current series. Obsoleted
307
- // and historic RFCs were previously excluded, so "is RFC N still current?"
308
- // had no offline answer and forced a datatracker lookup. They are added here
309
- // marked `_obsoleted` (with obsoleted_by populated) so the resolver can say
310
- // "Historic, superseded by RFC X" offline. UNKNOWN-status index rows
311
- // (placeholders / not-issued numbers) are still skipped.
249
+ // New rows come from the full index, not just the current series: obsoleted and
250
+ // historic RFCs land marked `_obsoleted` with obsoleted_by populated, so the
251
+ // resolver answers "Historic, superseded by RFC X" offline. UNKNOWN is skipped.
312
252
  for (const e of backfillable) {
313
253
  const id = `RFC-${e.num}`;
314
254
  // Existing rows handled in the first-pass backfill above.
@@ -347,10 +287,8 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
347
287
  existing.add(id);
348
288
  added++;
349
289
  }
350
- // Only restamp _meta + write when something actually changed. A genuine
351
- // no-op leaves the file byte-identical so the daily refresh doesn't emit a
352
- // spurious _meta-only diff (and so the freshness gates stay honest — a
353
- // wall-clock restamp on an unchanged catalog masks real staleness).
290
+ // Restamp _meta and write only on a real change: restamping an unchanged
291
+ // catalog masks real staleness from the freshness gates.
354
292
  const changed = added > 0 || backfilledCount > 0 || statusBumped > 0;
355
293
  if (dry) {
356
294
  console.log(`[refresh-upstream:rfc] DRY-RUN: +${added} new, ${backfilledCount} backfilled, ${statusBumped} status bumps.`);
@@ -369,8 +307,6 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
369
307
  return { added, statusBumped, backfilled: backfilledCount };
370
308
  }
371
309
 
372
- // ---------------- ATT&CK ----------------
373
-
374
310
  const ATTACK_SRC = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json";
375
311
  const ATTACK_TACTIC_NAME = {
376
312
  "reconnaissance": "Reconnaissance", "resource-development": "Resource Development",
@@ -383,10 +319,8 @@ const ATTACK_TACTIC_NAME = {
383
319
  "exfiltration": "Exfiltration", "impact": "Impact"
384
320
  };
385
321
 
386
- // Extract the full STIX-bundle context fields the AI needs to find
387
- // techniques by topic (not just by ID). description_full preserves the
388
- // MITRE description; description (short) is the first-sentence
389
- // extractive summary used by token-budgeted consumers.
322
+ // description_full keeps MITRE's text; description is the first-sentence
323
+ // summary that token-budgeted consumers read.
390
324
  function attackEntryFromStix(t, extRef) {
391
325
  const id = extRef.external_id;
392
326
  const tactics = (t.kill_chain_phases || [])
@@ -429,13 +363,9 @@ function backfillAttack(cur, fresh) {
429
363
  if (!cur[key] && val) { cur[key] = val; touched = true; }
430
364
  }
431
365
  };
432
- // v0.13.19: include description (short) + tactic in the backfill set.
433
- // Existing rows from the original 110-entry catalog often have only
434
- // {name, version} — they need tactic + short-description too, not just
435
- // the v0.13.18 description_full / platforms / detection additions.
436
366
  fillIfEmpty("description", fresh.description);
437
- // tactic: arrays only (existing rows may have a string tactic; do
438
- // not overwrite a stringified tactic with an array form).
367
+ // Arrays only: an existing row may carry a string tactic, which must not be
368
+ // overwritten with the array form.
439
369
  if ((!cur.tactic || (Array.isArray(cur.tactic) && cur.tactic.length === 0)) && Array.isArray(fresh.tactic) && fresh.tactic.length) {
440
370
  cur.tactic = fresh.tactic;
441
371
  touched = true;
@@ -462,12 +392,9 @@ async function refreshAttack({ dry = false, cap = Infinity, _deps = {} } = {}) {
462
392
  console.log("[refresh-upstream:attack] fetching MITRE ATT&CK STIX...");
463
393
  const body = await _fetchUrl(ATTACK_SRC);
464
394
  const stix = JSON.parse(body);
465
- // For NEW adds: live techniques only (skip revoked / deprecated).
466
- // For BACKFILL on existing rows: include revoked too — an operator-
467
- // curated row that references a now-revoked MITRE ID may still want
468
- // the context fields (name / description / platforms) from the
469
- // pre-revocation STIX record. Same logic as the RFC obsoleted-but-
470
- // backfillable two-pass design.
395
+ // New adds take live techniques only; the backfill pass includes revoked and
396
+ // deprecated ones, so a curated row on a revoked MITRE ID still gets its
397
+ // context fields from the pre-revocation record.
471
398
  const liveTechs = (stix.objects || []).filter(
472
399
  (o) => o.type === "attack-pattern" && !o.revoked && !o.x_mitre_deprecated
473
400
  );
@@ -488,8 +415,6 @@ async function refreshAttack({ dry = false, cap = Infinity, _deps = {} } = {}) {
488
415
  });
489
416
  let added = 0;
490
417
  let backfilled = 0;
491
- // First pass: backfill existing rows against the FULL technique set
492
- // (including revoked) so operator-curated rows still get context.
493
418
  for (const t of backfillTechs) {
494
419
  const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
495
420
  if (!extRef || !extRef.external_id) continue;
@@ -502,7 +427,6 @@ async function refreshAttack({ dry = false, cap = Infinity, _deps = {} } = {}) {
502
427
  backfilled++;
503
428
  }
504
429
  }
505
- // Second pass: add new entries from live techniques only.
506
430
  for (const t of techs) {
507
431
  const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
508
432
  if (!extRef || !extRef.external_id) continue;
@@ -525,8 +449,6 @@ async function refreshAttack({ dry = false, cap = Infinity, _deps = {} } = {}) {
525
449
  return { added, backfilled };
526
450
  }
527
451
 
528
- // ---------------- ICS-ATT&CK ----------------
529
-
530
452
  const ICS_ATTACK_SRC = "https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json";
531
453
  const ICS_TACTIC_NAME = {
532
454
  "initial-access": "Initial Access (ICS)",
@@ -604,8 +526,6 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
604
526
  return { added, backfilled };
605
527
  }
606
528
 
607
- // ---------------- ATLAS ----------------
608
-
609
529
  const ATLAS_SRC = "https://raw.githubusercontent.com/mitre-atlas/atlas-navigator-data/main/dist/stix-atlas.json";
610
530
 
611
531
  function atlasTactic(phases) {
@@ -659,14 +579,10 @@ function backfillAtlas(cur, fresh) {
659
579
  if (!cur[key] && val) { cur[key] = val; touched = true; }
660
580
  }
661
581
  };
662
- // Parity with backfillAttack: include the short description + tactic in the
663
- // backfill set. Existing curated ATLAS rows often carry only {name} and need
664
- // the short description + tactic too, not just description_full/platforms/etc.
582
+ // Mirrors backfillAttack's set, short description and tactic included.
665
583
  fillIfEmpty("description", fresh.description);
666
- // tactic: atlasEntryFromStix() emits a STRING for a single-tactic technique
667
- // and an array for multi-tactic, so backfill both forms. fillIfEmpty only
668
- // writes when cur is empty, so an existing (string OR array) tactic is never
669
- // overwritten — the common single-tactic case is no longer left unhydrated.
584
+ // atlasEntryFromStix emits a string for a single-tactic technique and an array
585
+ // for multi; fillIfEmpty writes only into an empty field, so both survive.
670
586
  fillIfEmpty("tactic", fresh.tactic);
671
587
  fillIfEmpty("description_full", fresh.description_full);
672
588
  fillIfEmpty("platforms", fresh.platforms);
@@ -726,9 +642,8 @@ async function refreshAtlas({ dry = false, _deps = {} } = {}) {
726
642
  added++;
727
643
  }
728
644
  if (dry) { console.log(`[refresh-upstream:atlas] DRY-RUN: +${added} new, ${backfilled} backfills${atlasVersion ? `, v${atlasVersion}` : ""}`); return { added, backfilled, atlasVersion }; }
729
- // A newly-detected ATLAS matrix version that differs from the recorded one
730
- // is itself a change (the catalog should bump atlas_version + last_updated
731
- // together), independent of any added/backfilled rows.
645
+ // A matrix version differing from the recorded one is itself a change, so
646
+ // atlas_version and last_updated bump together with no row edits at all.
732
647
  const versionChanged = !!(atlasVersion && local._meta && local._meta.atlas_version !== atlasVersion);
733
648
  const changed = added > 0 || backfilled > 0 || versionChanged;
734
649
  if (changed) {
@@ -745,8 +660,6 @@ async function refreshAtlas({ dry = false, _deps = {} } = {}) {
745
660
  return { added, backfilled, atlasVersion };
746
661
  }
747
662
 
748
- // ---------------- D3FEND ----------------
749
-
750
663
  const D3FEND_SRC = "https://d3fend.mitre.org/ontologies/d3fend.json";
751
664
 
752
665
  function d3fendTactic(parent) {
@@ -772,12 +685,9 @@ function d3fendIdList(t, field) {
772
685
  return arr.map((x) => (x && x["@id"]) ? String(x["@id"]).replace(/^d3f:/, "") : null).filter(Boolean);
773
686
  }
774
687
 
775
- // Strip a trailing period from an OWL d3fend-id: a few upstream artifact ids
776
- // (e.g. "D3A-C4.") carry a spurious terminal dot that no id token regex can
777
- // round-trip, leaving the entry unmatchable by the orphan/cross-ref scanners.
778
- // No legitimate d3fend technique id ends in a period. The SAME normalization
779
- // must key the catalog (existing.has / local[id]) as well as the entry payload,
780
- // or a refresh re-adds the period-keyed row every run (duplicate / churn).
688
+ // A few upstream ids ("D3A-C4.") carry a terminal dot that no id-token regex
689
+ // round-trips, leaving the entry unmatchable by the orphan and cross-ref
690
+ // scanners. It must key the catalog too, or every run re-adds the dotted row.
781
691
  function normD3fendId(rawId) {
782
692
  return typeof rawId === "string" ? rawId.replace(/\.$/, "") : rawId;
783
693
  }
@@ -804,9 +714,6 @@ function d3fendEntryFromOwl(t) {
804
714
  description: desc || `D3FEND defensive technique ${id}. Reference: https://d3fend.mitre.org/technique/${id}/`,
805
715
  description_full: fullDesc || null,
806
716
  synonyms,
807
- // Relationship fields — what offensive techniques this counters,
808
- // what defensive technique it enables / falls under, the parent
809
- // narrower/broader classes for hierarchical lookup.
810
717
  defends_against: d3fendIdList(t, "d3f:defends-against"),
811
718
  counters: d3fendIdList(t, "d3f:counters"),
812
719
  enables: d3fendIdList(t, "d3f:enables"),
@@ -885,8 +792,6 @@ async function refreshD3fend({ dry = false, cap = Infinity, _deps = {} } = {}) {
885
792
  return { added, backfilled };
886
793
  }
887
794
 
888
- // ---------------- CLI dispatcher ----------------
889
-
890
795
  const SOURCES = {
891
796
  rfc: { name: "ietf-rfc-index", run: refreshRfc },
892
797
  attack: { name: "mitre-attack-stix", run: refreshAttack },
@@ -939,11 +844,9 @@ module.exports = {
939
844
  refreshD3fend,
940
845
  SOURCES,
941
846
  runCli,
942
- // Exported for regression tests: fetchUrl's status/redirect handling and
943
- // writeCatalog's atomicity are load-bearing fail-closed properties.
847
+ // Exported for tests: fail-closed fetch behaviour and the atomic write.
944
848
  fetchUrl,
945
849
  writeCatalog,
946
- // Exported for regression tests: backfillAtlas must mirror backfillAttack's
947
- // description + array-tactic backfill on curated rows.
850
+ // Exported for tests: backfillAtlas must keep mirroring backfillAttack.
948
851
  backfillAtlas
949
852
  };