@blamejs/exceptd-skills 0.19.33 → 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.
- package/CHANGELOG.md +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
//
|
|
46
|
-
//
|
|
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
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
-
//
|
|
103
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
257
|
-
//
|
|
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
|
-
//
|
|
270
|
-
//
|
|
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
|
-
//
|
|
279
|
-
//
|
|
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
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
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
|
-
//
|
|
351
|
-
//
|
|
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
|
-
//
|
|
387
|
-
//
|
|
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
|
-
//
|
|
438
|
-
//
|
|
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
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
667
|
-
//
|
|
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
|
|
730
|
-
//
|
|
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
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
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
|
|
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
|
|
947
|
-
// description + array-tactic backfill on curated rows.
|
|
850
|
+
// Exported for tests: backfillAtlas must keep mirroring backfillAttack.
|
|
948
851
|
backfillAtlas
|
|
949
852
|
};
|