@blamejs/exceptd-skills 0.13.18 → 0.13.19

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.
@@ -190,53 +190,56 @@ async function refreshRfc({ dry = false } = {}) {
190
190
  const body = await fetchUrl(RFC_SRC);
191
191
  console.log(`[refresh-upstream:rfc] index size: ${(body.length / 1e6).toFixed(2)} MB`);
192
192
  const re = /<rfc-entry>([\s\S]*?)<\/rfc-entry>/g;
193
- const entries = [];
193
+ const entries = []; // current — eligible for new-add
194
+ const backfillable = []; // any-status — eligible for backfill on existing rows
194
195
  let m;
195
196
  while ((m = re.exec(body)) !== null) {
196
197
  const e = parseRfcEntry(m[1]);
197
198
  if (!e) continue;
199
+ backfillable.push(e);
198
200
  if (e.obsoleted || e.status === "HISTORIC" || e.status === "UNKNOWN") continue;
199
201
  entries.push(e);
200
202
  }
201
- console.log(`[refresh-upstream:rfc] current entries: ${entries.length}`);
203
+ console.log(`[refresh-upstream:rfc] current entries: ${entries.length} (+ ${backfillable.length - entries.length} obsoleted/historic available for backfill on existing rows)`);
202
204
  const cat = loadCatalog("rfc-references.json");
203
205
  const existing = new Set(Object.keys(cat).filter((k) => k !== "_meta"));
204
- let added = 0, statusBumped = 0;
205
- for (const e of entries) {
206
+ let added = 0, statusBumped = 0, backfilledCount = 0;
207
+ // First pass: backfill ALL existing rows from the broader entry set
208
+ // (including obsoleted historics). Operator may have curated an
209
+ // obsoleted RFC in for documentation; we still want abstract/authors.
210
+ for (const e of backfillable) {
206
211
  const id = `RFC-${e.num}`;
207
- if (existing.has(id)) {
208
- const cur = cat[id];
209
- let touched = false;
210
- // Status bumps only on auto-imported rows — operator-curated rows
211
- // may have a deliberately-different status (e.g. tracking the
212
- // older standard while a successor is in Proposed Standard limbo).
213
- if (cur && cur._auto_imported && cur.status !== RFC_STATUS_MAP[e.status]) {
214
- cur.status = RFC_STATUS_MAP[e.status];
215
- touched = true;
216
- statusBumped++;
217
- }
218
- // Backfill context-search fields on ANY row (auto-imported or
219
- // operator-curated) when the local field is absent. Backfill is
220
- // additive — it never overwrites a value an operator chose, only
221
- // fills holes left by the pre-v0.13.18 catalog shape that stored
222
- // only title + status.
223
- if (cur && !cur.abstract && e.abstract) { cur.abstract = e.abstract; touched = true; }
224
- if (cur && (!cur.keywords || cur.keywords.length === 0) && e.keywords.length) { cur.keywords = e.keywords; touched = true; }
225
- if (cur && !cur.area && e.area) { cur.area = e.area; touched = true; }
226
- if (cur && !cur.working_group && e.wg) { cur.working_group = e.wg; touched = true; }
227
- if (cur && !cur.stream && e.stream) { cur.stream = e.stream; touched = true; }
228
- if (cur && (!cur.authors || cur.authors.length === 0) && e.authors.length) { cur.authors = e.authors; touched = true; }
229
- if (cur && !cur.doi && e.doi) { cur.doi = e.doi; touched = true; }
230
- if (cur && !cur.page_count && e.pageCount) { cur.page_count = e.pageCount; touched = true; }
231
- if (cur && (!cur.obsoletes || cur.obsoletes.length === 0) && e.obsoletes.length) { cur.obsoletes = e.obsoletes; touched = true; }
232
- if (cur && (!cur.updates || cur.updates.length === 0) && e.updates.length) { cur.updates = e.updates; touched = true; }
233
- if (cur && (!cur.updated_by || cur.updated_by.length === 0) && e.updatedBy.length) { cur.updated_by = e.updatedBy; touched = true; }
234
- if (cur && (!cur.is_also || cur.is_also.length === 0) && e.isAlso.length) { cur.is_also = e.isAlso; touched = true; }
235
- if (cur && !cur.txt_url) { cur.txt_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.txt`; touched = true; }
236
- if (cur && !cur.html_url) { cur.html_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.html`; touched = true; }
237
- if (touched && cur) cur.last_verified = TODAY;
238
- continue;
212
+ if (!existing.has(id)) continue;
213
+ const cur = cat[id];
214
+ if (!cur) continue;
215
+ let touched = false;
216
+ if (cur._auto_imported && cur.status !== RFC_STATUS_MAP[e.status]) {
217
+ cur.status = RFC_STATUS_MAP[e.status];
218
+ touched = true;
219
+ statusBumped++;
239
220
  }
221
+ if (!cur.abstract && e.abstract) { cur.abstract = e.abstract; touched = true; }
222
+ if ((!cur.keywords || cur.keywords.length === 0) && e.keywords.length) { cur.keywords = e.keywords; touched = true; }
223
+ if (!cur.area && e.area) { cur.area = e.area; touched = true; }
224
+ if (!cur.working_group && e.wg) { cur.working_group = e.wg; touched = true; }
225
+ if (!cur.stream && e.stream) { cur.stream = e.stream; touched = true; }
226
+ if ((!cur.authors || cur.authors.length === 0) && e.authors.length) { cur.authors = e.authors; touched = true; }
227
+ if (!cur.doi && e.doi) { cur.doi = e.doi; touched = true; }
228
+ if (!cur.page_count && e.pageCount) { cur.page_count = e.pageCount; touched = true; }
229
+ if ((!cur.obsoletes || cur.obsoletes.length === 0) && e.obsoletes.length) { cur.obsoletes = e.obsoletes; touched = true; }
230
+ if ((!cur.updates || cur.updates.length === 0) && e.updates.length) { cur.updates = e.updates; touched = true; }
231
+ if ((!cur.updated_by || cur.updated_by.length === 0) && e.updatedBy.length) { cur.updated_by = e.updatedBy; touched = true; }
232
+ if ((!cur.obsoleted_by || cur.obsoleted_by.length === 0) && e.obsoletedBy.length) { cur.obsoleted_by = e.obsoletedBy; touched = true; }
233
+ if ((!cur.is_also || cur.is_also.length === 0) && e.isAlso.length) { cur.is_also = e.isAlso; touched = true; }
234
+ if (!cur.txt_url) { cur.txt_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.txt`; touched = true; }
235
+ if (!cur.html_url) { cur.html_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.html`; touched = true; }
236
+ if (touched) { cur.last_verified = TODAY; backfilledCount++; }
237
+ }
238
+ // Second pass: add new "current" entries that weren't in the catalog.
239
+ for (const e of entries) {
240
+ const id = `RFC-${e.num}`;
241
+ // Existing rows handled in the first-pass backfill above.
242
+ if (existing.has(id)) continue;
240
243
  cat[id] = {
241
244
  number: e.num,
242
245
  title: e.title,
@@ -272,12 +275,12 @@ async function refreshRfc({ dry = false } = {}) {
272
275
  cat._meta.last_threat_review = TODAY;
273
276
  }
274
277
  if (dry) {
275
- console.log(`[refresh-upstream:rfc] DRY-RUN: +${added} new, ${statusBumped} status bumps.`);
276
- return { added, statusBumped };
278
+ console.log(`[refresh-upstream:rfc] DRY-RUN: +${added} new, ${backfilledCount} backfilled, ${statusBumped} status bumps.`);
279
+ return { added, statusBumped, backfilled: backfilledCount };
277
280
  }
278
281
  writeCatalog("rfc-references.json", cat);
279
- console.log(`[ok] rfc-references.json: +${added} entries, ${statusBumped} status bumps (now ${existing.size} total)`);
280
- return { added, statusBumped };
282
+ console.log(`[ok] rfc-references.json: +${added} entries, ${backfilledCount} backfilled, ${statusBumped} status bumps (now ${existing.size} total)`);
283
+ return { added, statusBumped, backfilled: backfilledCount };
281
284
  }
282
285
 
283
286
  // ---------------- ATT&CK ----------------
@@ -340,6 +343,17 @@ function backfillAttack(cur, fresh) {
340
343
  if (!cur[key] && val) { cur[key] = val; touched = true; }
341
344
  }
342
345
  };
346
+ // v0.13.19: include description (short) + tactic in the backfill set.
347
+ // Existing rows from the original 110-entry catalog often have only
348
+ // {name, version} — they need tactic + short-description too, not just
349
+ // the v0.13.18 description_full / platforms / detection additions.
350
+ fillIfEmpty("description", fresh.description);
351
+ // tactic: arrays only (existing rows may have a string tactic; do
352
+ // not overwrite a stringified tactic with an array form).
353
+ if ((!cur.tactic || (Array.isArray(cur.tactic) && cur.tactic.length === 0)) && Array.isArray(fresh.tactic) && fresh.tactic.length) {
354
+ cur.tactic = fresh.tactic;
355
+ touched = true;
356
+ }
343
357
  fillIfEmpty("description_full", fresh.description_full);
344
358
  fillIfEmpty("platforms", fresh.platforms);
345
359
  fillIfEmpty("data_sources", fresh.data_sources);
@@ -359,10 +373,20 @@ async function refreshAttack({ dry = false, cap = Infinity } = {}) {
359
373
  console.log("[refresh-upstream:attack] fetching MITRE ATT&CK STIX...");
360
374
  const body = await fetchUrl(ATTACK_SRC);
361
375
  const stix = JSON.parse(body);
362
- const techs = (stix.objects || []).filter(
376
+ // For NEW adds: live techniques only (skip revoked / deprecated).
377
+ // For BACKFILL on existing rows: include revoked too — an operator-
378
+ // curated row that references a now-revoked MITRE ID may still want
379
+ // the context fields (name / description / platforms) from the
380
+ // pre-revocation STIX record. Same logic as the RFC obsoleted-but-
381
+ // backfillable two-pass design.
382
+ const liveTechs = (stix.objects || []).filter(
363
383
  (o) => o.type === "attack-pattern" && !o.revoked && !o.x_mitre_deprecated
364
384
  );
365
- console.log(`[refresh-upstream:attack] STIX live techniques: ${techs.length}`);
385
+ const backfillTechs = (stix.objects || []).filter(
386
+ (o) => o.type === "attack-pattern"
387
+ );
388
+ console.log(`[refresh-upstream:attack] STIX live techniques: ${liveTechs.length} (+ ${backfillTechs.length - liveTechs.length} revoked/deprecated available for backfill on existing rows)`);
389
+ const techs = liveTechs;
366
390
  const local = loadCatalog("attack-techniques.json");
367
391
  const existing = new Set(Object.keys(local).filter((k) => k !== "_meta"));
368
392
  techs.sort((a, b) => {
@@ -375,28 +399,106 @@ async function refreshAttack({ dry = false, cap = Infinity } = {}) {
375
399
  });
376
400
  let added = 0;
377
401
  let backfilled = 0;
402
+ // First pass: backfill existing rows against the FULL technique set
403
+ // (including revoked) so operator-curated rows still get context.
404
+ for (const t of backfillTechs) {
405
+ const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
406
+ if (!extRef || !extRef.external_id) continue;
407
+ const id = extRef.external_id;
408
+ if (!existing.has(id)) continue;
409
+ const fresh = attackEntryFromStix(t, extRef);
410
+ const cur = local[id];
411
+ if (backfillAttack(cur, fresh)) {
412
+ cur.last_verified = TODAY;
413
+ backfilled++;
414
+ }
415
+ }
416
+ // Second pass: add new entries from live techniques only.
378
417
  for (const t of techs) {
379
418
  const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
380
419
  if (!extRef || !extRef.external_id) continue;
381
420
  const id = extRef.external_id;
421
+ if (existing.has(id)) continue;
422
+ if (added >= cap) continue;
423
+ local[id] = attackEntryFromStix(t, extRef);
424
+ existing.add(id);
425
+ added++;
426
+ }
427
+ if (dry) { console.log(`[refresh-upstream:attack] DRY-RUN: +${added} new, ${backfilled} context backfills`); return { added, backfilled }; }
428
+ if (local._meta) { local._meta.last_updated = TODAY; local._meta.last_threat_review = TODAY; }
429
+ writeCatalog("attack-techniques.json", local);
430
+ console.log(`[ok] attack-techniques.json: +${added} entries, ${backfilled} context backfills (now ${existing.size} total)`);
431
+ return { added, backfilled };
432
+ }
433
+
434
+ // ---------------- ICS-ATT&CK ----------------
435
+
436
+ const ICS_ATTACK_SRC = "https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json";
437
+ const ICS_TACTIC_NAME = {
438
+ "initial-access": "Initial Access (ICS)",
439
+ "execution": "Execution (ICS)",
440
+ "persistence": "Persistence (ICS)",
441
+ "privilege-escalation": "Privilege Escalation (ICS)",
442
+ "evasion": "Evasion (ICS)",
443
+ "discovery": "Discovery (ICS)",
444
+ "lateral-movement": "Lateral Movement (ICS)",
445
+ "collection": "Collection (ICS)",
446
+ "command-and-control": "Command and Control (ICS)",
447
+ "inhibit-response-function": "Inhibit Response Function",
448
+ "impair-process-control": "Impair Process Control",
449
+ "impact": "Impact (ICS)"
450
+ };
451
+
452
+ async function refreshIcsAttack({ dry = false, cap = Infinity } = {}) {
453
+ console.log("[refresh-upstream:ics-attack] fetching MITRE ICS-attack STIX...");
454
+ const body = await fetchUrl(ICS_ATTACK_SRC);
455
+ const stix = JSON.parse(body);
456
+ const techs = (stix.objects || []).filter(
457
+ (o) => o.type === "attack-pattern" && !o.revoked && !o.x_mitre_deprecated
458
+ );
459
+ console.log(`[refresh-upstream:ics-attack] STIX live ICS techniques: ${techs.length}`);
460
+ const local = loadCatalog("attack-techniques.json");
461
+ const existing = new Set(Object.keys(local).filter((k) => k !== "_meta"));
462
+ let added = 0, backfilled = 0;
463
+ for (const t of techs) {
464
+ const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-ics-attack" || r.source_name === "mitre-attack");
465
+ if (!extRef || !extRef.external_id) continue;
466
+ const id = extRef.external_id;
467
+ const tactics = (t.kill_chain_phases || [])
468
+ .filter((p) => (p.kill_chain_name || "").includes("ics"))
469
+ .map((p) => ICS_TACTIC_NAME[p.phase_name] || `${p.phase_name} (ICS)`);
470
+ const fullDesc = String(t.description || "").replace(/\s+/g, " ").trim();
471
+ let shortDesc = fullDesc.split(/\.\s/)[0];
472
+ if (shortDesc.length > 500) shortDesc = shortDesc.slice(0, 497) + "...";
473
+ if (shortDesc && !shortDesc.endsWith(".")) shortDesc += ".";
474
+ const fresh = {
475
+ id, name: t.name, version: "ics-attack-v15",
476
+ tactic: tactics,
477
+ description: shortDesc,
478
+ description_full: fullDesc,
479
+ platforms: Array.isArray(t.x_mitre_platforms) ? t.x_mitre_platforms : [],
480
+ detection: (t.x_mitre_detection || "").replace(/\s+/g, " ").trim() || null,
481
+ reference_url: extRef.url || `https://attack.mitre.org/techniques/${id}/`,
482
+ stix_id: t.id || null,
483
+ last_verified: TODAY,
484
+ _auto_imported: true,
485
+ _intake_method: "mitre-ics-attack-stix",
486
+ _matrix: "ics-attack"
487
+ };
382
488
  if (existing.has(id)) {
383
- const fresh = attackEntryFromStix(t, extRef);
384
489
  const cur = local[id];
385
- if (backfillAttack(cur, fresh)) {
386
- cur.last_verified = TODAY;
387
- backfilled++;
388
- }
490
+ if (backfillAttack(cur, fresh)) { cur.last_verified = TODAY; backfilled++; }
389
491
  continue;
390
492
  }
391
493
  if (added >= cap) continue;
392
- local[id] = attackEntryFromStix(t, extRef);
494
+ local[id] = fresh;
393
495
  existing.add(id);
394
496
  added++;
395
497
  }
396
- if (dry) { console.log(`[refresh-upstream:attack] DRY-RUN: +${added} new, ${backfilled} context backfills`); return { added, backfilled }; }
498
+ if (dry) { console.log(`[refresh-upstream:ics-attack] DRY-RUN: +${added} new, ${backfilled} backfills`); return { added, backfilled }; }
397
499
  if (local._meta) { local._meta.last_updated = TODAY; local._meta.last_threat_review = TODAY; }
398
500
  writeCatalog("attack-techniques.json", local);
399
- console.log(`[ok] attack-techniques.json: +${added} entries, ${backfilled} context backfills (now ${existing.size} total)`);
501
+ console.log(`[ok] attack-techniques.json: +${added} ICS entries, ${backfilled} backfills (now ${existing.size} total)`);
400
502
  return { added, backfilled };
401
503
  }
402
504
 
@@ -645,10 +747,11 @@ async function refreshD3fend({ dry = false, cap = Infinity } = {}) {
645
747
  // ---------------- CLI dispatcher ----------------
646
748
 
647
749
  const SOURCES = {
648
- rfc: { name: "ietf-rfc-index", run: refreshRfc },
649
- attack: { name: "mitre-attack-stix", run: refreshAttack },
650
- atlas: { name: "mitre-atlas-stix", run: refreshAtlas },
651
- d3fend: { name: "mitre-d3fend-owl", run: refreshD3fend }
750
+ rfc: { name: "ietf-rfc-index", run: refreshRfc },
751
+ attack: { name: "mitre-attack-stix", run: refreshAttack },
752
+ "ics-attack": { name: "mitre-ics-attack-stix", run: refreshIcsAttack },
753
+ atlas: { name: "mitre-atlas-stix", run: refreshAtlas },
754
+ d3fend: { name: "mitre-d3fend-owl", run: refreshD3fend }
652
755
  };
653
756
 
654
757
  function parseArgs(argv) {
@@ -690,6 +793,7 @@ if (require.main === module) {
690
793
  module.exports = {
691
794
  refreshRfc,
692
795
  refreshAttack,
796
+ refreshIcsAttack,
693
797
  refreshAtlas,
694
798
  refreshD3fend,
695
799
  SOURCES,