@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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* scripts/audit-catalog-gaps.js
|
|
5
|
+
*
|
|
6
|
+
* Walks every data/*.json catalog and surfaces three classes of gap:
|
|
7
|
+
*
|
|
8
|
+
* 1. missing-context entries that exist but lack one of the
|
|
9
|
+
* documented context-search fields (e.g. RFC
|
|
10
|
+
* without abstract; ATT&CK technique without
|
|
11
|
+
* platforms; CVE without iocs)
|
|
12
|
+
*
|
|
13
|
+
* 2. dangling-ref forward references from one catalog into
|
|
14
|
+
* another that do not resolve (e.g. CVE
|
|
15
|
+
* entry's cwe_refs cites CWE-XXX but the
|
|
16
|
+
* local cwe-catalog does not carry that ID)
|
|
17
|
+
*
|
|
18
|
+
* 3. draft-debt per-catalog count of _auto_imported rows
|
|
19
|
+
* relative to operator-curated rows. High
|
|
20
|
+
* draft-debt = bulk-imported surface that has
|
|
21
|
+
* not been refined yet.
|
|
22
|
+
*
|
|
23
|
+
* Output: structured JSON to stdout (default) or human-readable summary
|
|
24
|
+
* with `--pretty`. Returns exit 0 in --warn-only mode (default); exit
|
|
25
|
+
* 1 in --strict mode if any class triggers.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node scripts/audit-catalog-gaps.js # JSON
|
|
29
|
+
* node scripts/audit-catalog-gaps.js --pretty # human
|
|
30
|
+
* node scripts/audit-catalog-gaps.js --strict # exit 1 on gap
|
|
31
|
+
* node scripts/audit-catalog-gaps.js --catalog cve # one catalog
|
|
32
|
+
* node scripts/audit-catalog-gaps.js --class missing-context
|
|
33
|
+
*
|
|
34
|
+
* npm: `npm run audit-catalog-gaps`
|
|
35
|
+
*
|
|
36
|
+
* Design note: the gap analyzer is a separate detection plane from
|
|
37
|
+
* lib/validate-cve-catalog.js (schema validation, predeploy gate) and
|
|
38
|
+
* scripts/refresh-reverse-refs.js (forward/reverse-ref currency). The
|
|
39
|
+
* validator polices what's strictly required by the schema; the gap
|
|
40
|
+
* analyzer polices the recommended-but-not-required context envelope
|
|
41
|
+
* that lets an AI consumer find an entry by topic instead of by ID.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require("fs");
|
|
45
|
+
const path = require("path");
|
|
46
|
+
|
|
47
|
+
const ROOT = path.join(__dirname, "..");
|
|
48
|
+
const DATA = path.join(ROOT, "data");
|
|
49
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
50
|
+
|
|
51
|
+
// Per-catalog required context fields. Each entry in the array is a
|
|
52
|
+
// field path (dot-separated for nested) and a non-emptiness predicate.
|
|
53
|
+
// Pillar / Class / Pillar-abstraction CWEs and similar can opt out via
|
|
54
|
+
// the suppression key on the entry (_gap_skip: { fields: [...] }).
|
|
55
|
+
const SPEC = {
|
|
56
|
+
"cve-catalog": {
|
|
57
|
+
file: "cve-catalog.json",
|
|
58
|
+
idShape: /^(CVE-|MAL-|BUG-|GHSA-|SNYK-)/,
|
|
59
|
+
required_context: [
|
|
60
|
+
{ field: "iocs", check: (v) => v && (
|
|
61
|
+
(Array.isArray(v.payload_artifacts) && v.payload_artifacts.length) ||
|
|
62
|
+
(Array.isArray(v.behavioral) && v.behavioral.length)
|
|
63
|
+
), label: "iocs.payload_artifacts or iocs.behavioral" },
|
|
64
|
+
{ field: "framework_control_gaps", check: (v) => v && Object.keys(v).length > 0, label: "framework_control_gaps" },
|
|
65
|
+
{ field: "attack_refs", check: (v) => Array.isArray(v) && v.length > 0, label: "attack_refs" },
|
|
66
|
+
{ field: "cwe_refs", check: (v) => Array.isArray(v) && v.length > 0, label: "cwe_refs" },
|
|
67
|
+
{ field: "verification_sources", check: (v) => Array.isArray(v) && v.length > 0, label: "verification_sources" }
|
|
68
|
+
],
|
|
69
|
+
refs: [
|
|
70
|
+
{ field: "cwe_refs", target: "cwe-catalog.json", item: true },
|
|
71
|
+
{ field: "attack_refs", target: "attack-techniques.json", item: true },
|
|
72
|
+
{ field: "atlas_refs", target: "atlas-ttps.json", item: true },
|
|
73
|
+
{ field: "framework_control_gaps", target: "framework-control-gaps.json", keys: true }
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"cwe-catalog": {
|
|
77
|
+
file: "cwe-catalog.json",
|
|
78
|
+
idShape: /^CWE-\d+$/,
|
|
79
|
+
required_context: [
|
|
80
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
81
|
+
{ field: "abstraction", check: (v) => typeof v === "string" && v.length > 0, label: "abstraction" },
|
|
82
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 20, label: "description (>20 chars)" }
|
|
83
|
+
],
|
|
84
|
+
refs: []
|
|
85
|
+
},
|
|
86
|
+
"attack-techniques": {
|
|
87
|
+
file: "attack-techniques.json",
|
|
88
|
+
idShape: /^T\d{4}(\.\d{3})?$/,
|
|
89
|
+
required_context: [
|
|
90
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
91
|
+
{ field: "tactic", check: (v) => (Array.isArray(v) ? v.length > 0 : typeof v === "string" && v.length > 0), label: "tactic" },
|
|
92
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description (short)" },
|
|
93
|
+
{ field: "platforms", check: (v) => Array.isArray(v) && v.length > 0, label: "platforms" }
|
|
94
|
+
],
|
|
95
|
+
refs: []
|
|
96
|
+
},
|
|
97
|
+
"atlas-ttps": {
|
|
98
|
+
file: "atlas-ttps.json",
|
|
99
|
+
idShape: /^AML\.T\d{4}(\.\d{3})?$/,
|
|
100
|
+
required_context: [
|
|
101
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
102
|
+
{ field: "tactic", check: (v) => (Array.isArray(v) ? v.length > 0 : typeof v === "string" && v.length > 0), label: "tactic" },
|
|
103
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description" }
|
|
104
|
+
],
|
|
105
|
+
refs: []
|
|
106
|
+
},
|
|
107
|
+
"d3fend-catalog": {
|
|
108
|
+
file: "d3fend-catalog.json",
|
|
109
|
+
idShape: /^D3-/,
|
|
110
|
+
required_context: [
|
|
111
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
112
|
+
{ field: "tactic", check: (v) => typeof v === "string" && v.length > 0, label: "tactic" },
|
|
113
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description" }
|
|
114
|
+
],
|
|
115
|
+
refs: []
|
|
116
|
+
},
|
|
117
|
+
"rfc-references": {
|
|
118
|
+
file: "rfc-references.json",
|
|
119
|
+
idShape: /^(RFC-\d+|DRAFT-|ISO-|CSAF-)/,
|
|
120
|
+
required_context: [
|
|
121
|
+
{ field: "title", check: (v) => typeof v === "string" && v.length > 0, label: "title" },
|
|
122
|
+
{ field: "status", check: (v) => typeof v === "string" && v.length > 0, label: "status" },
|
|
123
|
+
{ field: "abstract", check: (v) => typeof v === "string" && v.length > 20, label: "abstract (>20 chars)" }
|
|
124
|
+
],
|
|
125
|
+
refs: []
|
|
126
|
+
},
|
|
127
|
+
"framework-control-gaps": {
|
|
128
|
+
file: "framework-control-gaps.json",
|
|
129
|
+
idShape: /^[A-Z]/,
|
|
130
|
+
required_context: [
|
|
131
|
+
{ field: "framework", check: (v) => typeof v === "string" && v.length > 0, label: "framework" },
|
|
132
|
+
{ field: "control_id", check: (v) => typeof v === "string" && v.length > 0, label: "control_id" },
|
|
133
|
+
{ field: "control_name", check: (v) => typeof v === "string" && v.length > 0, label: "control_name" },
|
|
134
|
+
{ field: "real_requirement", check: (v) => typeof v === "string" && v.length > 20, label: "real_requirement (>20 chars)" },
|
|
135
|
+
{ field: "theater_test", check: (v) => v && typeof v.claim === "string" && typeof v.test === "string", label: "theater_test{claim,test}" },
|
|
136
|
+
{ field: "evidence_cves", check: (v) => Array.isArray(v) && v.length > 0, label: "evidence_cves" }
|
|
137
|
+
],
|
|
138
|
+
refs: []
|
|
139
|
+
},
|
|
140
|
+
"zeroday-lessons": {
|
|
141
|
+
file: "zeroday-lessons.json",
|
|
142
|
+
idShape: /^(CVE-|MAL-|BUG-)/,
|
|
143
|
+
required_context: [
|
|
144
|
+
{ field: "attack_vector", check: (v) => v && typeof v.description === "string" && v.description.length > 20, label: "attack_vector.description" },
|
|
145
|
+
{ field: "framework_coverage", check: (v) => v && Object.keys(v).length > 0, label: "framework_coverage" },
|
|
146
|
+
{ field: "new_control_requirements", check: (v) => Array.isArray(v) && v.length > 0, label: "new_control_requirements" }
|
|
147
|
+
],
|
|
148
|
+
refs: []
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function loadCatalog(name) {
|
|
153
|
+
return JSON.parse(fs.readFileSync(path.join(DATA, name), "utf8"));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function inspect(catalogKey) {
|
|
157
|
+
const spec = SPEC[catalogKey];
|
|
158
|
+
if (!spec) throw new Error(`unknown catalog: ${catalogKey}`);
|
|
159
|
+
const cat = loadCatalog(spec.file);
|
|
160
|
+
const ids = Object.keys(cat).filter((k) => k !== "_meta");
|
|
161
|
+
const report = {
|
|
162
|
+
catalog: catalogKey,
|
|
163
|
+
entries: ids.length,
|
|
164
|
+
auto_imported: 0,
|
|
165
|
+
operator_curated: 0,
|
|
166
|
+
missing_context: [],
|
|
167
|
+
dangling_refs: []
|
|
168
|
+
};
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
if (!spec.idShape.test(id)) continue;
|
|
171
|
+
const e = cat[id];
|
|
172
|
+
if (!e) continue;
|
|
173
|
+
if (e._auto_imported) report.auto_imported++;
|
|
174
|
+
else report.operator_curated++;
|
|
175
|
+
const skip = e._gap_skip && Array.isArray(e._gap_skip.fields) ? new Set(e._gap_skip.fields) : new Set();
|
|
176
|
+
for (const r of spec.required_context) {
|
|
177
|
+
if (skip.has(r.field)) continue;
|
|
178
|
+
if (!r.check(e[r.field])) {
|
|
179
|
+
report.missing_context.push({ id, field: r.field, label: r.label });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inspectRefs(allCatalogs) {
|
|
187
|
+
const findings = [];
|
|
188
|
+
const cveCat = allCatalogs["cve-catalog"];
|
|
189
|
+
const cweCat = allCatalogs["cwe-catalog"];
|
|
190
|
+
const attCat = allCatalogs["attack-techniques"];
|
|
191
|
+
const atlCat = allCatalogs["atlas-ttps"];
|
|
192
|
+
const fwCat = allCatalogs["framework-control-gaps"];
|
|
193
|
+
// Build presence sets keyed by id (sans _meta).
|
|
194
|
+
const cweSet = new Set(Object.keys(cweCat).filter((k) => k !== "_meta"));
|
|
195
|
+
const attSet = new Set(Object.keys(attCat).filter((k) => k !== "_meta"));
|
|
196
|
+
const atlSet = new Set(Object.keys(atlCat).filter((k) => k !== "_meta"));
|
|
197
|
+
const fwSet = new Set(Object.keys(fwCat).filter((k) => k !== "_meta"));
|
|
198
|
+
for (const id of Object.keys(cveCat)) {
|
|
199
|
+
if (id === "_meta") continue;
|
|
200
|
+
const e = cveCat[id];
|
|
201
|
+
if (!e) continue;
|
|
202
|
+
for (const ref of (e.cwe_refs || [])) {
|
|
203
|
+
if (!cweSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "cwe-catalog", missing: ref });
|
|
204
|
+
}
|
|
205
|
+
for (const ref of (e.attack_refs || [])) {
|
|
206
|
+
if (!attSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "attack-techniques", missing: ref });
|
|
207
|
+
}
|
|
208
|
+
for (const ref of (e.atlas_refs || [])) {
|
|
209
|
+
if (!atlSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "atlas-ttps", missing: ref });
|
|
210
|
+
}
|
|
211
|
+
const fcg = e.framework_control_gaps || {};
|
|
212
|
+
for (const key of Object.keys(fcg)) {
|
|
213
|
+
if (!fwSet.has(key)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "framework-control-gaps", missing: key });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return findings;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseArgs(argv) {
|
|
220
|
+
const out = { pretty: false, strict: false, catalog: null, klass: null };
|
|
221
|
+
for (let i = 2; i < argv.length; i++) {
|
|
222
|
+
const a = argv[i];
|
|
223
|
+
if (a === "--pretty") out.pretty = true;
|
|
224
|
+
else if (a === "--strict") out.strict = true;
|
|
225
|
+
else if (a === "--catalog") out.catalog = argv[++i];
|
|
226
|
+
else if (a === "--class") out.klass = argv[++i];
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function emitPretty(report) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
lines.push("Catalog gap audit");
|
|
234
|
+
lines.push("=================");
|
|
235
|
+
for (const r of report.per_catalog) {
|
|
236
|
+
lines.push(`\n[${r.catalog}] entries=${r.entries} auto-imported=${r.auto_imported} operator-curated=${r.operator_curated}`);
|
|
237
|
+
if (r.missing_context.length === 0) {
|
|
238
|
+
lines.push(" ✓ context complete on every entry");
|
|
239
|
+
} else {
|
|
240
|
+
// Group by field for tidier output.
|
|
241
|
+
const byField = new Map();
|
|
242
|
+
for (const m of r.missing_context) {
|
|
243
|
+
if (!byField.has(m.field)) byField.set(m.field, []);
|
|
244
|
+
byField.get(m.field).push(m.id);
|
|
245
|
+
}
|
|
246
|
+
for (const [field, ids] of byField) {
|
|
247
|
+
lines.push(` missing ${field} on ${ids.length} entries: ${ids.slice(0, 5).join(", ")}${ids.length > 5 ? ` ... +${ids.length - 5}` : ""}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
lines.push("\nCross-catalog dangling refs:");
|
|
252
|
+
if (report.dangling_refs.length === 0) {
|
|
253
|
+
lines.push(" ✓ every cross-ref resolves");
|
|
254
|
+
} else {
|
|
255
|
+
const byTarget = new Map();
|
|
256
|
+
for (const f of report.dangling_refs) {
|
|
257
|
+
const key = `${f.source_catalog}.${f.target_catalog}`;
|
|
258
|
+
if (!byTarget.has(key)) byTarget.set(key, []);
|
|
259
|
+
byTarget.get(key).push(`${f.source_id} → ${f.missing}`);
|
|
260
|
+
}
|
|
261
|
+
for (const [k, list] of byTarget) {
|
|
262
|
+
lines.push(` ${k}: ${list.length} dangling`);
|
|
263
|
+
for (const l of list.slice(0, 5)) lines.push(` ${l}`);
|
|
264
|
+
if (list.length > 5) lines.push(` ... +${list.length - 5}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
lines.push("\nDraft debt (auto-imported / total):");
|
|
268
|
+
for (const r of report.per_catalog) {
|
|
269
|
+
const pct = r.entries === 0 ? 0 : ((r.auto_imported / r.entries) * 100).toFixed(1);
|
|
270
|
+
lines.push(` ${r.catalog.padEnd(28)} ${r.auto_imported} / ${r.entries} (${pct}%)`);
|
|
271
|
+
}
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Valid finding-class names for the `--class` filter. The pretty + JSON
|
|
276
|
+
// emitters always include every section, but counts and strict-exit
|
|
277
|
+
// gating respect the active filter.
|
|
278
|
+
const VALID_CLASSES = new Set(["missing-context", "dangling-ref", "draft-debt"]);
|
|
279
|
+
|
|
280
|
+
function main() {
|
|
281
|
+
const opts = parseArgs(process.argv);
|
|
282
|
+
if (opts.klass && !VALID_CLASSES.has(opts.klass)) {
|
|
283
|
+
console.error(`unknown class: ${opts.klass} valid: ${[...VALID_CLASSES].join(", ")}`);
|
|
284
|
+
process.exitCode = 2;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const catalogKeys = opts.catalog ? [opts.catalog] : Object.keys(SPEC);
|
|
288
|
+
const perCatalog = [];
|
|
289
|
+
const allLoaded = {};
|
|
290
|
+
for (const k of catalogKeys) {
|
|
291
|
+
if (!SPEC[k]) {
|
|
292
|
+
console.error(`unknown catalog: ${k} valid: ${Object.keys(SPEC).join(", ")}`);
|
|
293
|
+
process.exitCode = 2;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
perCatalog.push(inspect(k));
|
|
297
|
+
allLoaded[k] = loadCatalog(SPEC[k].file);
|
|
298
|
+
}
|
|
299
|
+
// Load all needed catalogs for cross-ref pass even when --catalog scoped.
|
|
300
|
+
for (const k of Object.keys(SPEC)) if (!allLoaded[k]) allLoaded[k] = loadCatalog(SPEC[k].file);
|
|
301
|
+
const dangling = opts.catalog && opts.catalog !== "cve-catalog" ? [] : inspectRefs(allLoaded);
|
|
302
|
+
|
|
303
|
+
// Apply the --class filter before counts + strict-exit gating.
|
|
304
|
+
// Missing-context findings on per_catalog and dangling_refs are the
|
|
305
|
+
// two policed classes; draft-debt is informational-only (the audit
|
|
306
|
+
// surfaces draft-debt but it does not fail strict mode by design).
|
|
307
|
+
const filteredPerCatalog = opts.klass === "dangling-ref" || opts.klass === "draft-debt"
|
|
308
|
+
? perCatalog.map((r) => ({ ...r, missing_context: [] }))
|
|
309
|
+
: perCatalog;
|
|
310
|
+
const filteredDangling = opts.klass === "missing-context" || opts.klass === "draft-debt"
|
|
311
|
+
? []
|
|
312
|
+
: dangling;
|
|
313
|
+
|
|
314
|
+
const report = {
|
|
315
|
+
generated_at: TODAY,
|
|
316
|
+
class_filter: opts.klass || null,
|
|
317
|
+
per_catalog: filteredPerCatalog,
|
|
318
|
+
dangling_refs: filteredDangling,
|
|
319
|
+
totals: {
|
|
320
|
+
catalogs: filteredPerCatalog.length,
|
|
321
|
+
entries: filteredPerCatalog.reduce((n, r) => n + r.entries, 0),
|
|
322
|
+
missing_context: filteredPerCatalog.reduce((n, r) => n + r.missing_context.length, 0),
|
|
323
|
+
dangling_refs: filteredDangling.length
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
if (opts.pretty) {
|
|
327
|
+
process.stdout.write(emitPretty(report) + "\n");
|
|
328
|
+
} else {
|
|
329
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
330
|
+
}
|
|
331
|
+
if (opts.strict && (report.totals.missing_context > 0 || report.totals.dangling_refs > 0)) {
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (require.main === module) main();
|
|
337
|
+
|
|
338
|
+
module.exports = { SPEC, inspect, inspectRefs };
|
|
@@ -337,15 +337,23 @@ function extractCveIocChanges(beforeStr, afterStr) {
|
|
|
337
337
|
for (const id of ids) {
|
|
338
338
|
if (!/^CVE-\d{4}-\d+/.test(id)) continue;
|
|
339
339
|
// v0.13.18: skip bulk-imported entries. Auto-imported rows carry stub
|
|
340
|
-
// IoCs by design
|
|
341
|
-
//
|
|
342
|
-
// per-entry IoCs are not the operator-curated surface the diff-
|
|
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.
|
|
340
|
+
// IoCs by design; their per-entry IoCs are not the operator-curated
|
|
341
|
+
// surface the diff-coverage gate is designed to police.
|
|
346
342
|
const beforeAuto = !!(before[id] && before[id]._auto_imported);
|
|
347
343
|
const afterAuto = !!(after[id] && after[id]._auto_imported);
|
|
348
344
|
if (beforeAuto && afterAuto) continue;
|
|
345
|
+
// v0.13.19: also skip operator-curated rows whose IoCs are flagged
|
|
346
|
+
// as stubs (`_iocs_stub: true` — generic placeholder added by the
|
|
347
|
+
// gap-fix pass when an entry was missing iocs entirely). When an
|
|
348
|
+
// operator later curates real IoCs the diff-coverage check fires
|
|
349
|
+
// normally because the curation step removes _iocs_stub.
|
|
350
|
+
const beforeStub = !!(before[id] && before[id]._iocs_stub);
|
|
351
|
+
const afterStub = !!(after[id] && after[id]._iocs_stub);
|
|
352
|
+
// Existing entry going stub→curated (afterStub=false, beforeStub=true)
|
|
353
|
+
// is what we WANT to flag for review. Existing entry going non-stub→
|
|
354
|
+
// stub or stub→stub or absent→stub are all auto-fill events the
|
|
355
|
+
// diff-coverage gate should not police.
|
|
356
|
+
if (afterStub) continue;
|
|
349
357
|
const b = JSON.stringify((before[id] && before[id].iocs) || null);
|
|
350
358
|
const a = JSON.stringify((after[id] && after[id].iocs) || null);
|
|
351
359
|
if (b !== a) changed.add(id);
|
|
@@ -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); });
|