@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.
- package/CHANGELOG.md +38 -0
- package/data/_indexes/_meta.json +9 -9
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/_indexes/chains.json +14 -0
- package/data/_indexes/frequency.json +1 -0
- package/data/attack-techniques.json +2600 -109
- package/data/cve-catalog.json +1265 -305
- package/data/cwe-catalog.json +60 -1
- package/data/framework-control-gaps.json +504 -0
- package/data/rfc-references.json +286 -125
- package/data/zeroday-lessons.json +156 -24
- package/manifest.json +44 -44
- package/package.json +6 -2
- package/sbom.cdx.json +59 -29
- package/scripts/audit-catalog-gaps.js +338 -0
- package/scripts/check-test-coverage.js +14 -6
- package/scripts/refresh-mitre-ics-attack.js +15 -0
- package/scripts/refresh-upstream-catalogs.js +158 -54
|
@@ -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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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}
|
|
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}
|
|
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:
|
|
649
|
-
attack:
|
|
650
|
-
|
|
651
|
-
|
|
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,
|