@blamejs/exceptd-skills 0.13.18 → 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.
- package/CHANGELOG.md +79 -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 +147 -2678
- package/data/cwe-catalog.json +60 -1
- package/data/framework-control-gaps.json +252 -84
- package/data/rfc-references.json +286 -125
- package/data/zeroday-lessons.json +17 -2909
- package/lib/canonical-eq.js +88 -0
- package/lib/cve-regression-watcher.js +130 -9
- package/lib/source-advisories.js +9 -34
- package/lib/version-pins.js +73 -0
- package/lib/xml-tokenizer.js +344 -0
- package/manifest.json +44 -44
- package/package.json +6 -2
- package/sbom.cdx.json +108 -33
- package/scripts/audit-catalog-gaps.js +347 -0
- package/scripts/check-test-coverage.js +16 -10
- package/scripts/refresh-mitre-ics-attack.js +15 -0
- package/scripts/refresh-upstream-catalogs.js +171 -54
|
@@ -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,19 +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
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// coverage gate is designed to police. When an operator later
|
|
344
|
-
// curates the row, the next run will see the diff against the
|
|
345
|
-
// curated form and route through the normal coverage path.
|
|
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.
|
|
346
352
|
const beforeAuto = !!(before[id] && before[id]._auto_imported);
|
|
347
353
|
const afterAuto = !!(after[id] && after[id]._auto_imported);
|
|
348
354
|
if (beforeAuto && afterAuto) continue;
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
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);
|
|
352
358
|
}
|
|
353
359
|
return changed;
|
|
354
360
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* scripts/refresh-mitre-ics-attack.js
|
|
5
|
+
*
|
|
6
|
+
* Thin per-type wrapper for the MITRE ICS-attack STIX refresher. Logic
|
|
7
|
+
* lives in scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
|
|
8
|
+
*
|
|
9
|
+
* node scripts/refresh-mitre-ics-attack.js [--dry-run]
|
|
10
|
+
*
|
|
11
|
+
* Wired as `npm run refresh-mitre-ics-attack`.
|
|
12
|
+
*/
|
|
13
|
+
const { refreshIcsAttack } = require("./refresh-upstream-catalogs.js");
|
|
14
|
+
const dry = process.argv.includes("--dry-run");
|
|
15
|
+
refreshIcsAttack({ dry }).catch((e) => { console.error("[err]", e); process.exit(1); });
|
|
@@ -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) => {
|
|
@@ -190,53 +203,56 @@ async function refreshRfc({ dry = false } = {}) {
|
|
|
190
203
|
const body = await fetchUrl(RFC_SRC);
|
|
191
204
|
console.log(`[refresh-upstream:rfc] index size: ${(body.length / 1e6).toFixed(2)} MB`);
|
|
192
205
|
const re = /<rfc-entry>([\s\S]*?)<\/rfc-entry>/g;
|
|
193
|
-
const entries = [];
|
|
206
|
+
const entries = []; // current — eligible for new-add
|
|
207
|
+
const backfillable = []; // any-status — eligible for backfill on existing rows
|
|
194
208
|
let m;
|
|
195
209
|
while ((m = re.exec(body)) !== null) {
|
|
196
210
|
const e = parseRfcEntry(m[1]);
|
|
197
211
|
if (!e) continue;
|
|
212
|
+
backfillable.push(e);
|
|
198
213
|
if (e.obsoleted || e.status === "HISTORIC" || e.status === "UNKNOWN") continue;
|
|
199
214
|
entries.push(e);
|
|
200
215
|
}
|
|
201
|
-
console.log(`[refresh-upstream:rfc] current entries: ${entries.length}`);
|
|
216
|
+
console.log(`[refresh-upstream:rfc] current entries: ${entries.length} (+ ${backfillable.length - entries.length} obsoleted/historic available for backfill on existing rows)`);
|
|
202
217
|
const cat = loadCatalog("rfc-references.json");
|
|
203
218
|
const existing = new Set(Object.keys(cat).filter((k) => k !== "_meta"));
|
|
204
|
-
let added = 0, statusBumped = 0;
|
|
205
|
-
|
|
219
|
+
let added = 0, statusBumped = 0, backfilledCount = 0;
|
|
220
|
+
// First pass: backfill ALL existing rows from the broader entry set
|
|
221
|
+
// (including obsoleted historics). Operator may have curated an
|
|
222
|
+
// obsoleted RFC in for documentation; we still want abstract/authors.
|
|
223
|
+
for (const e of backfillable) {
|
|
206
224
|
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;
|
|
225
|
+
if (!existing.has(id)) continue;
|
|
226
|
+
const cur = cat[id];
|
|
227
|
+
if (!cur) continue;
|
|
228
|
+
let touched = false;
|
|
229
|
+
if (cur._auto_imported && cur.status !== RFC_STATUS_MAP[e.status]) {
|
|
230
|
+
cur.status = RFC_STATUS_MAP[e.status];
|
|
231
|
+
touched = true;
|
|
232
|
+
statusBumped++;
|
|
239
233
|
}
|
|
234
|
+
if (!cur.abstract && e.abstract) { cur.abstract = e.abstract; touched = true; }
|
|
235
|
+
if ((!cur.keywords || cur.keywords.length === 0) && e.keywords.length) { cur.keywords = e.keywords; touched = true; }
|
|
236
|
+
if (!cur.area && e.area) { cur.area = e.area; touched = true; }
|
|
237
|
+
if (!cur.working_group && e.wg) { cur.working_group = e.wg; touched = true; }
|
|
238
|
+
if (!cur.stream && e.stream) { cur.stream = e.stream; touched = true; }
|
|
239
|
+
if ((!cur.authors || cur.authors.length === 0) && e.authors.length) { cur.authors = e.authors; touched = true; }
|
|
240
|
+
if (!cur.doi && e.doi) { cur.doi = e.doi; touched = true; }
|
|
241
|
+
if (!cur.page_count && e.pageCount) { cur.page_count = e.pageCount; touched = true; }
|
|
242
|
+
if ((!cur.obsoletes || cur.obsoletes.length === 0) && e.obsoletes.length) { cur.obsoletes = e.obsoletes; touched = true; }
|
|
243
|
+
if ((!cur.updates || cur.updates.length === 0) && e.updates.length) { cur.updates = e.updates; touched = true; }
|
|
244
|
+
if ((!cur.updated_by || cur.updated_by.length === 0) && e.updatedBy.length) { cur.updated_by = e.updatedBy; touched = true; }
|
|
245
|
+
if ((!cur.obsoleted_by || cur.obsoleted_by.length === 0) && e.obsoletedBy.length) { cur.obsoleted_by = e.obsoletedBy; touched = true; }
|
|
246
|
+
if ((!cur.is_also || cur.is_also.length === 0) && e.isAlso.length) { cur.is_also = e.isAlso; touched = true; }
|
|
247
|
+
if (!cur.txt_url) { cur.txt_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.txt`; touched = true; }
|
|
248
|
+
if (!cur.html_url) { cur.html_url = `https://www.rfc-editor.org/rfc/rfc${e.num}.html`; touched = true; }
|
|
249
|
+
if (touched) { cur.last_verified = TODAY; backfilledCount++; }
|
|
250
|
+
}
|
|
251
|
+
// Second pass: add new "current" entries that weren't in the catalog.
|
|
252
|
+
for (const e of entries) {
|
|
253
|
+
const id = `RFC-${e.num}`;
|
|
254
|
+
// Existing rows handled in the first-pass backfill above.
|
|
255
|
+
if (existing.has(id)) continue;
|
|
240
256
|
cat[id] = {
|
|
241
257
|
number: e.num,
|
|
242
258
|
title: e.title,
|
|
@@ -272,12 +288,12 @@ async function refreshRfc({ dry = false } = {}) {
|
|
|
272
288
|
cat._meta.last_threat_review = TODAY;
|
|
273
289
|
}
|
|
274
290
|
if (dry) {
|
|
275
|
-
console.log(`[refresh-upstream:rfc] DRY-RUN: +${added} new, ${statusBumped} status bumps.`);
|
|
276
|
-
return { added, statusBumped };
|
|
291
|
+
console.log(`[refresh-upstream:rfc] DRY-RUN: +${added} new, ${backfilledCount} backfilled, ${statusBumped} status bumps.`);
|
|
292
|
+
return { added, statusBumped, backfilled: backfilledCount };
|
|
277
293
|
}
|
|
278
294
|
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 };
|
|
295
|
+
console.log(`[ok] rfc-references.json: +${added} entries, ${backfilledCount} backfilled, ${statusBumped} status bumps (now ${existing.size} total)`);
|
|
296
|
+
return { added, statusBumped, backfilled: backfilledCount };
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
// ---------------- ATT&CK ----------------
|
|
@@ -340,6 +356,17 @@ function backfillAttack(cur, fresh) {
|
|
|
340
356
|
if (!cur[key] && val) { cur[key] = val; touched = true; }
|
|
341
357
|
}
|
|
342
358
|
};
|
|
359
|
+
// v0.13.19: include description (short) + tactic in the backfill set.
|
|
360
|
+
// Existing rows from the original 110-entry catalog often have only
|
|
361
|
+
// {name, version} — they need tactic + short-description too, not just
|
|
362
|
+
// the v0.13.18 description_full / platforms / detection additions.
|
|
363
|
+
fillIfEmpty("description", fresh.description);
|
|
364
|
+
// tactic: arrays only (existing rows may have a string tactic; do
|
|
365
|
+
// not overwrite a stringified tactic with an array form).
|
|
366
|
+
if ((!cur.tactic || (Array.isArray(cur.tactic) && cur.tactic.length === 0)) && Array.isArray(fresh.tactic) && fresh.tactic.length) {
|
|
367
|
+
cur.tactic = fresh.tactic;
|
|
368
|
+
touched = true;
|
|
369
|
+
}
|
|
343
370
|
fillIfEmpty("description_full", fresh.description_full);
|
|
344
371
|
fillIfEmpty("platforms", fresh.platforms);
|
|
345
372
|
fillIfEmpty("data_sources", fresh.data_sources);
|
|
@@ -359,10 +386,20 @@ async function refreshAttack({ dry = false, cap = Infinity } = {}) {
|
|
|
359
386
|
console.log("[refresh-upstream:attack] fetching MITRE ATT&CK STIX...");
|
|
360
387
|
const body = await fetchUrl(ATTACK_SRC);
|
|
361
388
|
const stix = JSON.parse(body);
|
|
362
|
-
|
|
389
|
+
// For NEW adds: live techniques only (skip revoked / deprecated).
|
|
390
|
+
// For BACKFILL on existing rows: include revoked too — an operator-
|
|
391
|
+
// curated row that references a now-revoked MITRE ID may still want
|
|
392
|
+
// the context fields (name / description / platforms) from the
|
|
393
|
+
// pre-revocation STIX record. Same logic as the RFC obsoleted-but-
|
|
394
|
+
// backfillable two-pass design.
|
|
395
|
+
const liveTechs = (stix.objects || []).filter(
|
|
363
396
|
(o) => o.type === "attack-pattern" && !o.revoked && !o.x_mitre_deprecated
|
|
364
397
|
);
|
|
365
|
-
|
|
398
|
+
const backfillTechs = (stix.objects || []).filter(
|
|
399
|
+
(o) => o.type === "attack-pattern"
|
|
400
|
+
);
|
|
401
|
+
console.log(`[refresh-upstream:attack] STIX live techniques: ${liveTechs.length} (+ ${backfillTechs.length - liveTechs.length} revoked/deprecated available for backfill on existing rows)`);
|
|
402
|
+
const techs = liveTechs;
|
|
366
403
|
const local = loadCatalog("attack-techniques.json");
|
|
367
404
|
const existing = new Set(Object.keys(local).filter((k) => k !== "_meta"));
|
|
368
405
|
techs.sort((a, b) => {
|
|
@@ -375,28 +412,106 @@ async function refreshAttack({ dry = false, cap = Infinity } = {}) {
|
|
|
375
412
|
});
|
|
376
413
|
let added = 0;
|
|
377
414
|
let backfilled = 0;
|
|
415
|
+
// First pass: backfill existing rows against the FULL technique set
|
|
416
|
+
// (including revoked) so operator-curated rows still get context.
|
|
417
|
+
for (const t of backfillTechs) {
|
|
418
|
+
const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
|
|
419
|
+
if (!extRef || !extRef.external_id) continue;
|
|
420
|
+
const id = extRef.external_id;
|
|
421
|
+
if (!existing.has(id)) continue;
|
|
422
|
+
const fresh = attackEntryFromStix(t, extRef);
|
|
423
|
+
const cur = local[id];
|
|
424
|
+
if (backfillAttack(cur, fresh)) {
|
|
425
|
+
cur.last_verified = TODAY;
|
|
426
|
+
backfilled++;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Second pass: add new entries from live techniques only.
|
|
378
430
|
for (const t of techs) {
|
|
379
431
|
const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-attack");
|
|
380
432
|
if (!extRef || !extRef.external_id) continue;
|
|
381
433
|
const id = extRef.external_id;
|
|
434
|
+
if (existing.has(id)) continue;
|
|
435
|
+
if (added >= cap) continue;
|
|
436
|
+
local[id] = attackEntryFromStix(t, extRef);
|
|
437
|
+
existing.add(id);
|
|
438
|
+
added++;
|
|
439
|
+
}
|
|
440
|
+
if (dry) { console.log(`[refresh-upstream:attack] DRY-RUN: +${added} new, ${backfilled} context backfills`); return { added, backfilled }; }
|
|
441
|
+
if (local._meta) { local._meta.last_updated = TODAY; local._meta.last_threat_review = TODAY; }
|
|
442
|
+
writeCatalog("attack-techniques.json", local);
|
|
443
|
+
console.log(`[ok] attack-techniques.json: +${added} entries, ${backfilled} context backfills (now ${existing.size} total)`);
|
|
444
|
+
return { added, backfilled };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---------------- ICS-ATT&CK ----------------
|
|
448
|
+
|
|
449
|
+
const ICS_ATTACK_SRC = "https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json";
|
|
450
|
+
const ICS_TACTIC_NAME = {
|
|
451
|
+
"initial-access": "Initial Access (ICS)",
|
|
452
|
+
"execution": "Execution (ICS)",
|
|
453
|
+
"persistence": "Persistence (ICS)",
|
|
454
|
+
"privilege-escalation": "Privilege Escalation (ICS)",
|
|
455
|
+
"evasion": "Evasion (ICS)",
|
|
456
|
+
"discovery": "Discovery (ICS)",
|
|
457
|
+
"lateral-movement": "Lateral Movement (ICS)",
|
|
458
|
+
"collection": "Collection (ICS)",
|
|
459
|
+
"command-and-control": "Command and Control (ICS)",
|
|
460
|
+
"inhibit-response-function": "Inhibit Response Function",
|
|
461
|
+
"impair-process-control": "Impair Process Control",
|
|
462
|
+
"impact": "Impact (ICS)"
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
async function refreshIcsAttack({ dry = false, cap = Infinity } = {}) {
|
|
466
|
+
console.log("[refresh-upstream:ics-attack] fetching MITRE ICS-attack STIX...");
|
|
467
|
+
const body = await fetchUrl(ICS_ATTACK_SRC);
|
|
468
|
+
const stix = JSON.parse(body);
|
|
469
|
+
const techs = (stix.objects || []).filter(
|
|
470
|
+
(o) => o.type === "attack-pattern" && !o.revoked && !o.x_mitre_deprecated
|
|
471
|
+
);
|
|
472
|
+
console.log(`[refresh-upstream:ics-attack] STIX live ICS techniques: ${techs.length}`);
|
|
473
|
+
const local = loadCatalog("attack-techniques.json");
|
|
474
|
+
const existing = new Set(Object.keys(local).filter((k) => k !== "_meta"));
|
|
475
|
+
let added = 0, backfilled = 0;
|
|
476
|
+
for (const t of techs) {
|
|
477
|
+
const extRef = (t.external_references || []).find((r) => r.source_name === "mitre-ics-attack" || r.source_name === "mitre-attack");
|
|
478
|
+
if (!extRef || !extRef.external_id) continue;
|
|
479
|
+
const id = extRef.external_id;
|
|
480
|
+
const tactics = (t.kill_chain_phases || [])
|
|
481
|
+
.filter((p) => (p.kill_chain_name || "").includes("ics"))
|
|
482
|
+
.map((p) => ICS_TACTIC_NAME[p.phase_name] || `${p.phase_name} (ICS)`);
|
|
483
|
+
const fullDesc = String(t.description || "").replace(/\s+/g, " ").trim();
|
|
484
|
+
let shortDesc = fullDesc.split(/\.\s/)[0];
|
|
485
|
+
if (shortDesc.length > 500) shortDesc = shortDesc.slice(0, 497) + "...";
|
|
486
|
+
if (shortDesc && !shortDesc.endsWith(".")) shortDesc += ".";
|
|
487
|
+
const fresh = {
|
|
488
|
+
id, name: t.name, version: "ics-attack-v15",
|
|
489
|
+
tactic: tactics,
|
|
490
|
+
description: shortDesc,
|
|
491
|
+
description_full: fullDesc,
|
|
492
|
+
platforms: Array.isArray(t.x_mitre_platforms) ? t.x_mitre_platforms : [],
|
|
493
|
+
detection: (t.x_mitre_detection || "").replace(/\s+/g, " ").trim() || null,
|
|
494
|
+
reference_url: extRef.url || `https://attack.mitre.org/techniques/${id}/`,
|
|
495
|
+
stix_id: t.id || null,
|
|
496
|
+
last_verified: TODAY,
|
|
497
|
+
_auto_imported: true,
|
|
498
|
+
_intake_method: "mitre-ics-attack-stix",
|
|
499
|
+
_matrix: "ics-attack"
|
|
500
|
+
};
|
|
382
501
|
if (existing.has(id)) {
|
|
383
|
-
const fresh = attackEntryFromStix(t, extRef);
|
|
384
502
|
const cur = local[id];
|
|
385
|
-
if (backfillAttack(cur, fresh)) {
|
|
386
|
-
cur.last_verified = TODAY;
|
|
387
|
-
backfilled++;
|
|
388
|
-
}
|
|
503
|
+
if (backfillAttack(cur, fresh)) { cur.last_verified = TODAY; backfilled++; }
|
|
389
504
|
continue;
|
|
390
505
|
}
|
|
391
506
|
if (added >= cap) continue;
|
|
392
|
-
local[id] =
|
|
507
|
+
local[id] = fresh;
|
|
393
508
|
existing.add(id);
|
|
394
509
|
added++;
|
|
395
510
|
}
|
|
396
|
-
if (dry) { console.log(`[refresh-upstream:attack] DRY-RUN: +${added} new, ${backfilled}
|
|
511
|
+
if (dry) { console.log(`[refresh-upstream:ics-attack] DRY-RUN: +${added} new, ${backfilled} backfills`); return { added, backfilled }; }
|
|
397
512
|
if (local._meta) { local._meta.last_updated = TODAY; local._meta.last_threat_review = TODAY; }
|
|
398
513
|
writeCatalog("attack-techniques.json", local);
|
|
399
|
-
console.log(`[ok] attack-techniques.json: +${added} entries, ${backfilled}
|
|
514
|
+
console.log(`[ok] attack-techniques.json: +${added} ICS entries, ${backfilled} backfills (now ${existing.size} total)`);
|
|
400
515
|
return { added, backfilled };
|
|
401
516
|
}
|
|
402
517
|
|
|
@@ -645,10 +760,11 @@ async function refreshD3fend({ dry = false, cap = Infinity } = {}) {
|
|
|
645
760
|
// ---------------- CLI dispatcher ----------------
|
|
646
761
|
|
|
647
762
|
const SOURCES = {
|
|
648
|
-
rfc:
|
|
649
|
-
attack:
|
|
650
|
-
|
|
651
|
-
|
|
763
|
+
rfc: { name: "ietf-rfc-index", run: refreshRfc },
|
|
764
|
+
attack: { name: "mitre-attack-stix", run: refreshAttack },
|
|
765
|
+
"ics-attack": { name: "mitre-ics-attack-stix", run: refreshIcsAttack },
|
|
766
|
+
atlas: { name: "mitre-atlas-stix", run: refreshAtlas },
|
|
767
|
+
d3fend: { name: "mitre-d3fend-owl", run: refreshD3fend }
|
|
652
768
|
};
|
|
653
769
|
|
|
654
770
|
function parseArgs(argv) {
|
|
@@ -690,6 +806,7 @@ if (require.main === module) {
|
|
|
690
806
|
module.exports = {
|
|
691
807
|
refreshRfc,
|
|
692
808
|
refreshAttack,
|
|
809
|
+
refreshIcsAttack,
|
|
693
810
|
refreshAtlas,
|
|
694
811
|
refreshD3fend,
|
|
695
812
|
SOURCES,
|