@blamejs/exceptd-skills 0.19.1 → 0.19.3

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.
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * TTP reference-integrity gate.
6
+ *
7
+ * data/attack-techniques.json and data/atlas-ttps.json are the pinned copies of
8
+ * ATT&CK and ATLAS. Every other file that names a technique — countermeasure
9
+ * maps, DLP controls, playbooks, skill bodies, CVE entries — is referring INTO
10
+ * those two catalogs. This gate proves those references resolve.
11
+ *
12
+ * The failure it exists for: MITRE retires and renumbers techniques between
13
+ * releases. When a pin is bumped, the two source catalogs get remapped, but
14
+ * references living in other files are easy to miss — nothing dereferences them
15
+ * at runtime, so a stale id keeps rendering in operator output as if it were
16
+ * current. It points at a MITRE page that no longer resolves, and any control
17
+ * claiming to counter it is now mapped to nothing (AGENTS.md Hard Rule #4: no
18
+ * orphaned controls). A bumped pin left exactly this residue in the D3FEND and
19
+ * DLP maps, invisible to every other gate.
20
+ *
21
+ * The check is offline: it resolves references against the pinned catalogs, not
22
+ * against MITRE. That is deliberate — scripts/check-ttp-upstream.js is the
23
+ * network check that asks whether the PINS are current, and it cannot block a
24
+ * release because it needs connectivity. This one can block, because a
25
+ * reference that does not resolve against the pin we ship is broken no matter
26
+ * what upstream says.
27
+ *
28
+ * Exit codes: 0 all references resolve, 1 unresolved references found.
29
+ */
30
+
31
+ const fs = require("node:fs");
32
+ const path = require("node:path");
33
+
34
+ const ROOT = path.resolve(__dirname, "..");
35
+
36
+ /**
37
+ * ATT&CK ids are TNNNN[.NNN]; ATLAS ids are AML.TNNNN[.NNN]. The lookbehind
38
+ * matters: without it the ATT&CK alternative matches the "T0017" inside
39
+ * "AML.T0017" and reports a phantom bare-ATT&CK reference for every ATLAS id in
40
+ * the tree. It covers the hyphen as well, because ATLAS ids also appear in
41
+ * filenames, where the dot is not a legal separator.
42
+ */
43
+ const TTP_PATTERN = /\bAML\.T\d{4}(?:\.\d{3})?\b|(?<!AML[.-])\bT\d{4}(?:\.\d{3})?\b/g;
44
+
45
+ /** Files whose ids are definitions, not references. */
46
+ const SOURCE_CATALOGS = ["data/attack-techniques.json", "data/atlas-ttps.json"];
47
+
48
+ const SEARCH_ROOTS = ["data", "playbooks", "skills", "lib", "orchestrator", "bin"];
49
+ const SEARCH_EXTS = new Set([".json", ".md", ".js"]);
50
+ const SKIP_DIRS = new Set(["node_modules", "_indexes", "vendor", ".git"]);
51
+
52
+ /**
53
+ * Tokens that match the pattern without being references to a technique.
54
+ * Every entry needs a reason: an unexplained allowlist is how a real stale id
55
+ * eventually gets parked here to make the gate green.
56
+ */
57
+ const NOT_REFERENCES = [
58
+ {
59
+ id: "T1234",
60
+ file: "lib/gap-detectors.js",
61
+ why: "placeholder in a comment describing the reference-extraction pattern itself, not a claim about a technique",
62
+ },
63
+ ];
64
+
65
+ function loadKnownIds() {
66
+ const known = new Set();
67
+ for (const rel of SOURCE_CATALOGS) {
68
+ const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, rel), "utf8"));
69
+ for (const key of Object.keys(catalog)) {
70
+ if (key !== "_meta") known.add(key);
71
+ }
72
+ }
73
+ return known;
74
+ }
75
+
76
+ function* walk(dir) {
77
+ let names;
78
+ try {
79
+ names = fs.readdirSync(dir);
80
+ } catch {
81
+ return;
82
+ }
83
+ for (const name of names) {
84
+ if (SKIP_DIRS.has(name)) continue;
85
+ const full = path.join(dir, name);
86
+ let stat;
87
+ try {
88
+ stat = fs.statSync(full);
89
+ } catch {
90
+ continue;
91
+ }
92
+ if (stat.isDirectory()) {
93
+ yield* walk(full);
94
+ } else if (SEARCH_EXTS.has(path.extname(name))) {
95
+ yield full;
96
+ }
97
+ }
98
+ }
99
+
100
+ function allowed(id, rel) {
101
+ return NOT_REFERENCES.some((e) => e.id === id && e.file === rel);
102
+ }
103
+
104
+ function scan(root = ROOT) {
105
+ const known = loadKnownIds();
106
+ const sources = new Set(SOURCE_CATALOGS);
107
+ const unresolved = new Map();
108
+
109
+ for (const dirName of SEARCH_ROOTS) {
110
+ const dir = path.join(root, dirName);
111
+ if (!fs.existsSync(dir)) continue;
112
+
113
+ for (const file of walk(dir)) {
114
+ const rel = path.relative(root, file).replace(/\\/g, "/");
115
+ if (sources.has(rel)) continue;
116
+
117
+ const text = fs.readFileSync(file, "utf8");
118
+ for (const id of text.match(TTP_PATTERN) || []) {
119
+ if (known.has(id) || allowed(id, rel)) continue;
120
+ if (!unresolved.has(id)) unresolved.set(id, new Set());
121
+ unresolved.get(id).add(rel);
122
+ }
123
+ }
124
+ }
125
+
126
+ return { unresolved, knownCount: known.size };
127
+ }
128
+
129
+ function main() {
130
+ const { unresolved, knownCount } = scan();
131
+
132
+ if (unresolved.size) {
133
+ console.error("TTP reference integrity: FAIL");
134
+ for (const [id, files] of [...unresolved].sort((a, b) => a[0].localeCompare(b[0]))) {
135
+ console.error(` ${id} — not defined in the pinned catalogs`);
136
+ for (const f of [...files].sort()) console.error(` ${f}`);
137
+ }
138
+ console.error(
139
+ `\n${unresolved.size} unresolved ${unresolved.size === 1 ? "reference" : "references"}. ` +
140
+ `Either the id was retired upstream and these files still name it — repoint them at the ` +
141
+ `successor — or it is a technique the pinned catalogs do not carry yet, in which case import it.`
142
+ );
143
+ process.exitCode = 1;
144
+ return;
145
+ }
146
+
147
+ console.log(`TTP reference integrity: PASS — every referenced technique resolves against the ${knownCount} pinned ids`);
148
+ }
149
+
150
+ if (require.main === module) main();
151
+
152
+ module.exports = { scan, TTP_PATTERN, NOT_REFERENCES, loadKnownIds };
@@ -104,18 +104,32 @@ const COMMENT_EXEMPT = new Set([
104
104
  // need to name individual local-only files. Untracked-but-NOT-ignored files
105
105
  // ARE still scanned: a new file a contributor is about to commit is exactly
106
106
  // what the gate must catch. Computed via `git check-ignore` over the walked set.
107
+ // Returns the ignored subset, or NULL when git cannot answer.
108
+ //
109
+ // "No path matched" and "the question could not be asked" are different
110
+ // results and must not collapse into the same empty set. Without a repository
111
+ // — a build context that omits .git/, or git not installed — an empty set
112
+ // silently reclassifies every local-only file as part of the shipped surface,
113
+ // so the gate reports violations in files a clone never contains. Returning
114
+ // null lets the caller say it could not determine the surface instead of
115
+ // asserting a wrong one.
107
116
  function gitIgnoredSet(relPaths) {
108
117
  if (!relPaths.length) return new Set();
109
118
  try {
110
119
  const out = execFileSync("git", ["check-ignore", "--stdin"], {
111
120
  cwd: ROOT, input: relPaths.join("\n"), encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
121
+ stdio: ["pipe", "pipe", "pipe"],
112
122
  });
113
123
  return new Set(out.split(/\r?\n/).filter(Boolean));
114
124
  } catch (e) {
115
- // `git check-ignore --stdin` exits 1 when NO path is ignored (not an
116
- // error); any paths it did match are on stdout. Absent that, none ignored.
125
+ // Exit 1 with no stderr is git's way of saying "no path matched" — a real
126
+ // answer, and an empty set is correct. Anything else (git missing, not a
127
+ // repository, .git absent) means the question went unanswered.
128
+ const status = e && typeof e.status === "number" ? e.status : null;
129
+ const stderr = e && e.stderr ? String(e.stderr).trim() : "";
117
130
  const out = e && e.stdout ? String(e.stdout) : "";
118
- return new Set(out.split(/\r?\n/).filter(Boolean));
131
+ if (status === 1 && !stderr) return new Set(out.split(/\r?\n/).filter(Boolean));
132
+ return null;
119
133
  }
120
134
  }
121
135
 
@@ -185,12 +199,16 @@ function countLineViolations(rel) {
185
199
  function scanCurrent() {
186
200
  const files = walk(ROOT);
187
201
  const ignored = gitIgnoredSet(files);
202
+ // Without git the shipped surface is unknowable: local-only files are
203
+ // indistinguishable from tracked ones, so any result would be a guess.
204
+ // Report that rather than emit findings the baseline cannot be compared to.
205
+ if (ignored === null) return { byFile: {}, filenameViolations: [], surfaceUnknown: true };
188
206
  const byFile = {};
189
207
  const filenameViolations = [];
190
208
  for (const rel of files) {
191
- // Skip git-ignored, local-only files (a contributor's private working notes
192
- // that `git clone` never ships). Untracked-but-not-ignored files are still
193
- // scanned — a new file about to be committed is what the gate guards.
209
+ // Skip git-ignored, local-only files that `git clone` never ships.
210
+ // Untracked-but-not-ignored files are still scanned — a new file about to
211
+ // be committed is exactly what the gate guards.
194
212
  if (ignored.has(rel)) continue;
195
213
  if (FILENAME_VERSION_RE.test(rel)) filenameViolations.push(rel);
196
214
  const n = countLineViolations(rel);
@@ -229,6 +247,32 @@ function main() {
229
247
  const wantUpdate = process.argv.includes("--update-baseline");
230
248
  const current = scanCurrent();
231
249
 
250
+ if (current.surfaceUnknown) {
251
+ // Never write a baseline from a scan that could not tell shipped files from
252
+ // local ones — that would bake the wrong surface in permanently.
253
+ //
254
+ // Automation is the one place this must not degrade to a skip. This gate
255
+ // runs inside predeploy, and predeploy guards the publish job, so a
256
+ // silently-skipped run there stops enforcing on exactly the path that
257
+ // ships. Locally — a container built without .git, a tarball inspection —
258
+ // skipping is the honest answer, because the shipped surface genuinely is
259
+ // not knowable there and failing would only punish the harness.
260
+ const inAutomation = process.env.CI === "true" || !!process.env.GITHUB_ACTIONS;
261
+ if (inAutomation) {
262
+ console.error("[check-version-tags] FAIL — no git repository available, so the shipped");
263
+ console.error(" surface cannot be determined. In automation this is a failure, not a skip:");
264
+ console.error(" this gate runs inside predeploy, which guards publishing. Ensure the job");
265
+ console.error(" checks out git metadata (actions/checkout provides it by default).");
266
+ process.exitCode = 2;
267
+ return;
268
+ }
269
+ console.error("[check-version-tags] SKIPPED — no git repository available, so the shipped");
270
+ console.error(" surface cannot be determined. This is not a pass; run it where git metadata");
271
+ console.error(" is present. In CI the same condition fails instead.");
272
+ process.exitCode = 0;
273
+ return;
274
+ }
275
+
232
276
  if (wantUpdate) {
233
277
  writeBaseline(current);
234
278
  process.exitCode = 0;
@@ -222,6 +222,32 @@ const GATES = [
222
222
  args: [path.join(ROOT, "scripts", "check-framework-gap-coverage.js")],
223
223
  ciJobName: "Data integrity (catalog + manifest snapshot)",
224
224
  },
225
+ {
226
+ // TTP reference-integrity gate. The two pinned MITRE catalogs define the
227
+ // techniques; every other file naming one is referring into them. MITRE
228
+ // retires and renumbers between releases, and a reference living outside
229
+ // the source catalogs is never dereferenced at runtime — so a stale id
230
+ // keeps rendering in operator output, pointing at a page that no longer
231
+ // resolves and leaving any control mapped to it orphaned (Hard Rule #4).
232
+ // Resolves against the pin rather than the network, so it can block.
233
+ name: "TTP reference integrity (no orphaned technique ids)",
234
+ command: process.execPath,
235
+ args: [path.join(ROOT, "scripts", "check-ttp-references.js")],
236
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
237
+ },
238
+ {
239
+ // EPSS pair-consistency gate. A CVE's epss_score and epss_percentile come
240
+ // from the same daily publication, where the percentile is the score's
241
+ // rank — so sorting a publication's entries by score must sort them by
242
+ // percentile too. Refreshing one field without the other leaves an entry
243
+ // that passes every range and type check while ranking a CVE on a score
244
+ // that no longer supports it, which is precisely the input operators
245
+ // prioritise by. The ordering check catches that offline.
246
+ name: "EPSS score/percentile consistency",
247
+ command: process.execPath,
248
+ args: [path.join(ROOT, "scripts", "check-epss-consistency.js")],
249
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
250
+ },
225
251
  {
226
252
  // Version-tag drift gate. Compares the tracked tree against a
227
253
  // baseline snapshot of pre-existing `// vX.Y.Z` comments and
@@ -12,9 +12,14 @@
12
12
  * - bomFormat / specVersion CycloneDX 1.6
13
13
  * - serialNumber urn:uuid v4 derived from a stable
14
14
  * hash of (project name + version +
15
- * timestamp) so reruns produce a new
16
- * UUID per refresh.
17
- * - metadata.timestamp ISO 8601 of generation
15
+ * bundle digest), so identical content
16
+ * reproduces the identical UUID and a
17
+ * rerun that changed nothing is a no-op
18
+ * rather than a spurious diff.
19
+ * - metadata.timestamp the release date this version's
20
+ * CHANGELOG heading declares — NOT the
21
+ * wall-clock moment of generation, which
22
+ * would make the artifact irreproducible
18
23
  * - metadata.tools this script itself, version pulled
19
24
  * from package.json at refresh time
20
25
  * - metadata.component application entry for exceptd-skills,
@@ -145,13 +150,31 @@ const SELF_EXCLUDED = new Set(['sbom.cdx.json']);
145
150
  * side check for the cache. */
146
151
  const DERIVABLE_PREFIXES = ['data/_indexes/'];
147
152
 
153
+ /* Files npm puts in every tarball regardless of the `files` allowlist.
154
+ * package.json is never listed in `files` (npm adds it unconditionally), so
155
+ * expanding the allowlist alone left the one file that declares the bin
156
+ * entrypoint, the engines floor and the dependency set outside the hashed
157
+ * inventory — the SBOM described 247 of the 268 files an operator receives and
158
+ * said nothing about the gap. It is not derivable and not self-referential, so
159
+ * nothing but the omission kept it out. README.md and LICENSE get the same
160
+ * unconditional treatment from npm but are already named in `files`, so a
161
+ * union covers the general rule without double-counting them. sources/README.md
162
+ * is here for the same reason: npm collects README files it finds, `files` never
163
+ * names that directory, and it shipped unhashed.
164
+ *
165
+ * This list is maintained by hand, which is only safe because something else
166
+ * checks it: lib/validate-package.js compares the real `npm pack` output against
167
+ * this SBOM, so a file npm decides to ship that is missing here fails a gate
168
+ * instead of shipping silently. */
169
+ const ALWAYS_SHIPPED = ['package.json', 'sources/README.md'];
170
+
148
171
  function isDerivable(rel) {
149
172
  return DERIVABLE_PREFIXES.some((p) => rel === p.replace(/\/$/, '') || rel.startsWith(p));
150
173
  }
151
174
 
152
175
  function expandAllowlist(allowlist) {
153
176
  const abs = [];
154
- for (const entry of allowlist) {
177
+ for (const entry of [...allowlist, ...ALWAYS_SHIPPED]) {
155
178
  const full = path.join(REPO_ROOT, entry);
156
179
  if (!fs.existsSync(full)) continue; // tolerate a stale entry; predeploy gate flags
157
180
  const stat = fs.statSync(full);
@@ -171,6 +194,38 @@ function expandAllowlist(allowlist) {
171
194
  return rel;
172
195
  }
173
196
 
197
+ /* metadata.timestamp — the release date this version's CHANGELOG heading
198
+ * declares, as an ISO-8601 instant.
199
+ *
200
+ * The field has to be deterministic: the SBOM-currency gate compares the
201
+ * committed artifact against a freshly generated one, so a wall-clock value
202
+ * would differ on every run and the comparison could never mean anything.
203
+ * The previous approach kept determinism by folding the bundle hash into a
204
+ * date offset, but the offset was a full uint32 of SECONDS — a 136-year
205
+ * spread — so the field routinely landed a century or more in the future
206
+ * (the shipped value read 2147-11-20). Deterministic, and untrue: consumers
207
+ * that sort or age-check SBOMs read that as a real creation date.
208
+ *
209
+ * The CHANGELOG heading is both deterministic and true. Its format is already
210
+ * enforced by scripts/check-changelog-extract.js, which the release flow runs
211
+ * before this script, so the date is guaranteed present by the time a release
212
+ * regenerates the SBOM. Refusing is deliberate when it is absent — inventing a
213
+ * placeholder is what produced the 2147 date in the first place. */
214
+ function releaseTimestamp(version) {
215
+ const changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
216
+ const text = fs.readFileSync(changelogPath, 'utf8');
217
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
218
+ const m = text.match(new RegExp('^## ' + escaped + ' [—-] (\\d{4}-\\d{2}-\\d{2})\\s*$', 'm'));
219
+ if (!m) {
220
+ throw new Error(
221
+ `refresh-sbom: CHANGELOG.md has no dated heading for ${version}. ` +
222
+ `Add "## ${version} — YYYY-MM-DD" before regenerating — metadata.timestamp ` +
223
+ `is the release date, and there is no honest value to fall back to.`,
224
+ );
225
+ }
226
+ return `${m[1]}T00:00:00.000Z`;
227
+ }
228
+
174
229
  function toPosixRel(absPath) {
175
230
  return path
176
231
  .relative(REPO_ROOT, absPath)
@@ -303,12 +358,7 @@ function buildSbom() {
303
358
  // time of a refresh should read the file's mtime or refresh-report.json.
304
359
  const seed = `${pkg.name}@${pkg.version}@${bundleSha}`;
305
360
  const serialNumber = 'urn:uuid:' + uuidV4FromSeed(seed);
306
- // Synthetic ISO timestamp derived from the seed — preserves the
307
- // CycloneDX 1.6 metadata.timestamp schema requirement (must be an
308
- // ISO-8601 string) while remaining content-stable.
309
- const seedHash = crypto.createHash('sha256').update(seed).digest();
310
- const offsetSeconds = seedHash.readUInt32BE(0); // deterministic offset
311
- const timestamp = new Date(Date.UTC(2026, 0, 1) + offsetSeconds * 1000).toISOString();
361
+ const timestamp = releaseTimestamp(pkg.version);
312
362
 
313
363
  const dataflowInput = catalogs
314
364
  .map((c) => `data/${c}`)
@@ -366,6 +416,22 @@ function buildSbom() {
366
416
  name: 'exceptd:integrity:method',
367
417
  value: 'Ed25519 per-skill (lib/sign.js)',
368
418
  },
419
+ // An operator verifying the bundle holds the SBOM, not this script.
420
+ // Without these two properties the only record of what the inventory
421
+ // deliberately omits was a source comment they never see, so a partial
422
+ // inventory was indistinguishable from a complete one. State the
423
+ // uncovered prefix and the check that covers it instead.
424
+ {
425
+ name: 'exceptd:integrity:uncovered:prefix',
426
+ value: DERIVABLE_PREFIXES.join(','),
427
+ },
428
+ {
429
+ name: 'exceptd:integrity:uncovered:rationale',
430
+ value:
431
+ 'Regenerated by `npm run build-indexes` and mutated by the test suite, so a ' +
432
+ 'pinned per-file hash would race any run between generation and verification. ' +
433
+ 'Covered instead by the pre-computed-index freshness gate in `npm run predeploy`.',
434
+ },
369
435
  {
370
436
  name: 'exceptd:runtime:dependency:count',
371
437
  value: String(Object.keys(pkg.dependencies || {}).length),
@@ -409,4 +475,11 @@ if (require.main === module) {
409
475
  main();
410
476
  }
411
477
 
412
- module.exports = { buildSbom, expandAllowlist, bundleDigest };
478
+ module.exports = {
479
+ buildSbom,
480
+ expandAllowlist,
481
+ bundleDigest,
482
+ releaseTimestamp,
483
+ ALWAYS_SHIPPED,
484
+ DERIVABLE_PREFIXES,
485
+ };
@@ -295,8 +295,15 @@ function cmdPrepare(opts) {
295
295
  _ok("signed + indexes + snapshot + sbom regenerated");
296
296
 
297
297
  _section("test-count baseline");
298
- // Growth is fine (the gate only fails on shrinkage), but refreshing keeps
299
- // the canonical-count guard meaningful when a release adds test files.
298
+ // Check BEFORE refreshing. `--update-baseline` writes whatever it observes,
299
+ // so calling it unconditionally rebaselined a shrunken suite downward and the
300
+ // shrinkage gate — which runs later, in `gates` — then compared the new count
301
+ // against itself and passed. Releasing was the one path that disarmed the
302
+ // guard, which is the path it exists to guard. Run the gate first so a drop
303
+ // stops the release here; refresh only once it has agreed nothing was lost,
304
+ // which still captures growth. A deliberate removal is re-baselined by hand
305
+ // with the command the gate's own failure message prints.
306
+ _run("node", ["scripts/check-test-count.js"]);
300
307
  _run("node", ["scripts/check-test-count.js", "--update-baseline"]);
301
308
 
302
309
  _section("codebase-patterns currency (advisory)");
@@ -215,7 +215,7 @@ The skill produces a Defensive Countermeasure Map per input (CVE ID, ATLAS / ATT
215
215
  Example: "CVE — Linux kernel LPE. Canonical: CVE-2026-31431 (Copy Fail)."
216
216
 
217
217
  ## Offensive technique set (input to D3FEND query)
218
- - <AML.T0001-or-similar / T0001-or-similar / CWE-<id> list, with one-line descriptions>
218
+ - <AML.T####-or-similar / T####-or-similar / CWE-<id> list, with one-line descriptions>
219
219
 
220
220
  ## Defensive-coverage map
221
221
  | D3FEND ID | Name | Tactic (DiD layer) | Privilege scope | ZT posture | Deployed? | AI-pipeline applicable? | Framework controls partially mapped | Live-tunable? |
@@ -91,7 +91,7 @@ IR obligations span four layers — methodology (NIST 800-61r3, ISO 27035, SANS
91
91
  | ISO/IEC 27035-1:2023 | Information security incident management — Principles and process | Process model: plan & prepare → detect & report → assess & decide → respond → learn lessons. | Process-shaped, not playbook-shaped. No AI-class sub-types. No regulator-clock matrix. Conformance does not imply playbook completeness. |
92
92
  | ISO/IEC 27035-2:2023 | Guidelines to plan and prepare for incident response | Planning, team structure, communication. | Same process orientation. Mentions "external reporting obligations" without operationalizing EU CRA / NIS2 / AI Act / NYDFS clocks. |
93
93
  | NIST SP 800-53 rev 5 | IR-4 Incident Handling; IR-5 Incident Monitoring; IR-6 Incident Reporting; IR-8 Incident Response Plan | Method-neutral control objectives. | Per `framework-control-gaps.json` NIST-800-53-AC-2: AC-2 (Account Management) does not require AI-agent identity lifecycle management; identity-compromise (T1078) detection feeds IR but the underlying control gap leaves AI-agent service accounts under-instrumented. IR-4 says "implement an incident handling capability" without specifying AI-class handling. |
94
- | ISO/IEC 27001:2022 | A.5.24 Information security incident management planning and preparation; A.5.26 Response to information security incidents; A.8.16 Monitoring activities | Process-level requirements for incident response and monitoring. | Per `framework-control-gaps.json` ISO-27001-2022-A.8.16: monitoring requirements are technology-neutral but AI-system telemetry (prompt logs, embedding-store access, model-output classification) is not addressed. An ISO 27001-certified org with no AI-system monitoring is formally compliant and operationally blind to AML.T0051 / T0096 / T0017. |
94
+ | ISO/IEC 27001:2022 | A.5.24 Information security incident management planning and preparation; A.5.26 Response to information security incidents; A.8.16 Monitoring activities | Process-level requirements for incident response and monitoring. | Per `framework-control-gaps.json` ISO-27001-2022-A.8.16: monitoring requirements are technology-neutral but AI-system telemetry (prompt logs, embedding-store access, model-output classification) is not addressed. An ISO 27001-certified org with no AI-system monitoring is formally compliant and operationally blind to AML.T0051 / AML.T0096 / AML.T0017. |
95
95
  | SOC 2 | CC7 (System operations — security event detection, incident response) | Trust services criteria for anomaly detection and incident response. | Per `framework-control-gaps.json` SOC2-CC7-anomaly-detection: CC7 requires anomaly detection without specifying coverage for AI-API C2 (AML.T0096), training-data exfiltration (AML.T0017), or prompt-injection incident triggers (AML.T0051). Auditors test for "an anomaly detection system" without testing whether it covers AI traffic shape. Theater-prone. |
96
96
  | EU NIS2 Directive (2022/2555) | Art. 23 — incident notification | 24h early warning, 72h initial notification, 1-month final report to national CSIRT for significant incidents at essential/important entities. | Clocks are explicit; significance criteria are partly Member-State-defined; cross-border coordination via ENISA CSIRTs Network. The IR playbook must run the clocks; the directive does not define playbook content. |
97
97
  | EU DORA (Regulation 2022/2554) | Art. 17 (ICT incident management); Art. 19 (major ICT-related incident reporting); Art. 18 (classification) | Financial-entity-specific: 4h initial notification for major ICT incidents, 72h intermediate, 1-month final, all to competent authority (national + ECB/EIOPA/ESMA depending on entity). | DORA 4h is tighter than NIS2 24h; an entity in scope of both runs whichever is shorter. DORA RTS on classification (2024) defines "major" but the operational determination at the 4h mark requires triage maturity most entities lack. |
@@ -162,7 +162,7 @@ Before stepping through the IR program assessment, thread the three foundational
162
162
 
163
163
  **Defense in depth — IR as a multi-layer pipeline.** A real IR program is not the playbook document; it is the stack that produces the conditions for the playbook to fire and the conditions for it to succeed:
164
164
  - **Layer 1 — Preparation.** Playbook library (by ATT&CK technique + incident class + sector + jurisdiction), tabletop exercises (at least quarterly, scenarios drawn from current threat-intel feed), runbook tooling (SOAR + ticketing + comms), redundant logging (SIEM + DLP + identity + EDR + AI-system telemetry shipped to immutable store), legal and PR alignment, executive and board awareness, retainer with external IR firm.
165
- - **Layer 2 — Identification.** SIEM correlation rules mapped to ATT&CK; EDR / XDR on every endpoint and workload; UEBA for identity-anomaly detection; AI-incident detectors per `ai-c2-detection` for AML.T0096 / T0017 / T0051; threat-intel feed integration; honeypot / canary telemetry.
165
+ - **Layer 2 — Identification.** SIEM correlation rules mapped to ATT&CK; EDR / XDR on every endpoint and workload; UEBA for identity-anomaly detection; AI-incident detectors per `ai-c2-detection` for AML.T0096 / AML.T0017 / AML.T0051; threat-intel feed integration; honeypot / canary telemetry.
166
166
  - **Layer 3 — Containment.** Network-segment isolation capability (SDN, microsegmentation, firewall policy push); identity revocation capability (Conditional Access, OAuth-grant revocation, service-account rotation); endpoint isolation (EDR-driven network quarantine); AI-API egress block; cloud-workload pause/snapshot.
167
167
  - **Layer 4 — Eradication and recovery.** Artifact removal (file, registry, scheduled task, persistence mechanism); credential rotation at scope (privileged, service, AI-agent, OAuth app, API key); validated backup restore; AI-system rollback (model version, system prompt, RAG corpus state); service-level verification before declaring recovery.
168
168
  - **Layer 5 — Lessons learned.** Post-incident review (root-cause analysis using the Diamond Model and the Unified Kill Chain for adversary-narrative reconstruction); playbook update; detection-engineering refinement; control-gap filing per `framework-gap-analysis`; zero-day learning per `zeroday-gap-learn`; threat-model refresh per `threat-model-currency`; skill-update propagation per `skill-update-loop`.
@@ -192,7 +192,7 @@ When an incident fires, classify before responding. Classification dimensions:
192
192
  - **Incident class** — ransomware, data exfiltration, identity compromise, supply-chain, AI-system breach, business-email-compromise, DoS, insider, other.
193
193
  - **Impact severity** — confidentiality / integrity / availability per the org's incident-severity matrix.
194
194
  - **Jurisdictional notification clock** — per the matrix in Section 7. Which clocks start, when did they start (awareness moment), who is the named officer per clock.
195
- - **AI-class flag** — does the incident involve an AI system as victim, vector, or attacker? AI-as-victim: AML.T0051/T0017. AI-as-vector: AML.T0096. AI-as-attacker: agent-initiated unauthorized action.
195
+ - **AI-class flag** — does the incident involve an AI system as victim, vector, or attacker? AI-as-victim: AML.T0051/AML.T0017. AI-as-vector: AML.T0096. AI-as-attacker: agent-initiated unauthorized action.
196
196
  - **Sector flag** — does a sectoral framework apply (`sector-healthcare`, `sector-financial`, `sector-energy`, `sector-federal-government`)?
197
197
 
198
198
  ### Step 3 — Declaration and runbook activation
@@ -500,7 +500,7 @@ Four concrete tests distinguish a real IR program from IR theater. Run them in o
500
500
 
501
501
  > **Test 2 — Walk me through your EU DORA 4-hour initial-notification process, named officer included.** Substitute the tightest jurisdictional clock that applies to the org (DORA 4h for in-scope financial entities; SG CSA CCoP2.0 2h for SG CII; NERC CIP-008 1h for North American electric utilities; CERT-In 6h for India-operating entities; AU SOCI 12h; CRA Art. 11 24h for EU manufacturers). If the answer is "we'll figure it out when it happens" or "legal will handle it," the program will miss the clock during a real incident. The named officer must be identifiable, reachable on a documented out-of-band channel, and trained on the determination criteria for the relevant "significance" or "major" or "actively exploited" thresholds. If the org cannot produce the named officer's contact card and the decision tree they will use at 03:00 on a Saturday, the regulator-notification capability is theater.
502
502
 
503
- > **Test 3 — Do you have an AI-class incident playbook, and when was it last exercised?** Three failure modes signal theater: (a) "AI is just IT — we use our normal playbook" — the org has not engaged with AML.T0096 / T0017 / T0051 detection and containment specifics; (b) "we don't run AI systems" — verify against actual product surface (Copilot, Claude, ChatGPT, Gemini, AI features embedded in SaaS, internal agentic systems, RAG features); (c) "we have a draft playbook but never tested it" — untested AI-class playbooks fail at the same rate as untested conventional playbooks, but the failure modes are unfamiliar to the SOC. Particular smell: the AI-class playbook exists in the security team's shared drive but the AI-platform team and the data-science team have never seen it. AI-incident response requires cross-team rehearsal; AML.T0017 forensics requires data-science skills the SOC does not have.
503
+ > **Test 3 — Do you have an AI-class incident playbook, and when was it last exercised?** Three failure modes signal theater: (a) "AI is just IT — we use our normal playbook" — the org has not engaged with AML.T0096 / AML.T0017 / AML.T0051 detection and containment specifics; (b) "we don't run AI systems" — verify against actual product surface (Copilot, Claude, ChatGPT, Gemini, AI features embedded in SaaS, internal agentic systems, RAG features); (c) "we have a draft playbook but never tested it" — untested AI-class playbooks fail at the same rate as untested conventional playbooks, but the failure modes are unfamiliar to the SOC. Particular smell: the AI-class playbook exists in the security team's shared drive but the AI-platform team and the data-science team have never seen it. AI-incident response requires cross-team rehearsal; AML.T0017 forensics requires data-science skills the SOC does not have.
504
504
 
505
505
  > **Test 4 — Enumerate every jurisdictional notification clock that applies to your operations, name the officer for each, and produce the last drill record per clock.** If the org cannot enumerate clocks — clocks are discovered mid-incident, while running them late — the program will miss at least one in a real cross-jurisdictional event. The minimum enumeration for a multinational organization: EU (CRA Art. 11 + NIS2 Art. 23 + DORA Art. 19 if financial + AI Act Art. 73 if high-risk AI), UK (NIS + UK GDPR), AU (SOCI), JP (NISC + APPI), IL (INCD + PPA), SG (CSA CCoP2.0 + PDPC), IN (CERT-In), BR (LGPD), CN (MLPS + CSL + DSL + PIPL), US-NYDFS, US-HIPAA (if in scope), US-NERC (if in scope), AE (TDRA + DIFC DP). For each: clock, authority, channel, named officer, last drill. If the clocks live in a regulatory-comms team's binder rather than the IR runbook library, the program will run them out of sequence with the technical response and burn one of them.
506
506
 
@@ -521,7 +521,7 @@ IR consumes defensive controls across multiple D3FEND categories; the four cited
521
521
  | **D3-IOPR** (Input/Output Profiling) | AI-API egress correlation and SaaS-egress anomaly detection. For AI-system incidents, profiling the input (prompt) and output (response) distribution is the defensive surface that can detect AML.T0051 (anomalous prompt patterns), AML.T0017 (extraction-pattern queries), and AML.T0096 (C2-channel encoded payloads). | Identification layer (primary for AI-system incidents). | Scoped to the AI-incident specialist role; raw prompts and responses may contain confidential data and must be access-controlled per data-classification policy. | Default-suspect for prompt distributions outside the baseline; do not whitelist by source identity alone — verify per request. | High applicability — D3-IOPR is the highest-leverage D3FEND technique for AI-system incident detection and is the operational complement to D3-NTA when the egress is to a legitimate AI provider. |
522
522
  | **D3-CSPP** (Client-Server Payload Profiling) | C2 protocol detection during identification; AI-API content-layer detection for AML.T0096. Where the C2 channel is HTTPS to a legitimate service (Box, OneDrive, S3, AI provider), CSPP is the content-shape detection surface that catches the abuse pattern. | Identification layer. | Scoped to the detection-engineering and IR analyst roles; payload-content access controlled. | Default-suspect for novel payload shapes against baseline; verify-not-assume that previously-good clients have not been compromised. | Applies — particularly for AI-API C2 detection where TLS termination at an enterprise proxy enables payload-shape analysis of prompts and responses. |
523
523
 
524
- **No orphaned controls**: each D3FEND technique above maps to one or more incident classes in the TTP Mapping section (T1486 / T1041 / T1567 / T1078, AML.T0096 / T0017 / T0051). The defensive cross-walk in `defensive-countermeasure-mapping` covers the broader D3FEND ontology; this section names only the techniques operationally invoked during IR.
524
+ **No orphaned controls**: each D3FEND technique above maps to one or more incident classes in the TTP Mapping section (T1486 / T1041 / T1567 / T1078, AML.T0096 / AML.T0017 / AML.T0051). The defensive cross-walk in `defensive-countermeasure-mapping` covers the broader D3FEND ontology; this section names only the techniques operationally invoked during IR.
525
525
 
526
526
  **AI-pipeline statement**: D3FEND coverage of AI-incident defense is concentrated in D3-IOPR (input/output profiling) and the content-layer subset of D3-CSPP. The ephemeral-compute evidence-preservation problem is largely outside the D3FEND ontology as of mid-2026; the operational fix (continuous forensic-grade telemetry shipping to immutable store) is documented in `attack-surface-pentest` and `defensive-countermeasure-mapping` as a control gap pending ontology coverage.
527
527
 
@@ -117,7 +117,7 @@ Descriptions sourced from `data/atlas-ttps.json` (ATLAS v2026.07, released 2026-
117
117
  |---|---|---|---|
118
118
  | AML.T0010 | ML Supply Chain Compromise (sub-techniques: GPU Firmware, ML Framework, Model Repository, MCP Server) | Cross-cutting — touches every ingestion point: dependencies in training environment (AML.T0010.001), model pulls from Hugging Face / vendor registries (AML.T0010.002), MCP plugins in dev tooling (AML.T0010.003) | No framework mandates registry-side cryptographic verification of all ingested artifacts; the SLSA-style attestation chain for ML artifacts is draft, not required |
119
119
  | AML.T0018 | Manipulate AI Model (sub-techniques: Poison Training Data, Trojan Model via direct weight manipulation, Federated Learning Poisoning) | Training pipeline and post-training tampering — adversary modifies weights either through poisoned training data persisted into weights or through direct binary edit of an unsigned checkpoint | No framework requires model-weight signature verification at registry write and at deployment read; CWE-502 deserialization risk on `.pt` / `SavedModel` is unmapped to compliance control |
120
- | AML.T0020 | Poison Training Data (sub-techniques: Inject at Scale, Craft Targeted, RAG Knowledge Base Poisoning) | Data ingestion → feature store → training. Adversary contaminates training corpus to embed targeted misbehavior. Sub-technique AML.T0020.002 is RAG-side (see `rag-pipeline-security`); AML.T0020.000 / 001 are MLOps-side. | No framework requires training-data lineage attestation, source signing, or poisoning-detection scanning at ingestion. EU AI Act Art. 10 requires data-governance documentation but not cryptographic attestation. |
120
+ | AML.T0020 | Poison Training Data (sub-techniques: Inject at Scale, Craft Targeted, RAG Knowledge Base Poisoning) | Data ingestion → feature store → training. Adversary contaminates training corpus to embed targeted misbehavior. ATLAS carries this as a single technique with no sub-techniques; the RAG-corpus variant is covered in `rag-pipeline-security`, the training-pipeline variant here. | No framework requires training-data lineage attestation, source signing, or poisoning-detection scanning at ingestion. EU AI Act Art. 10 requires data-governance documentation but not cryptographic attestation. |
121
121
  | AML.T0043 | Craft Adversarial Data (White-Box, Black-Box, Physical) | Inference serving and feedback loop — adversary crafts inputs to either cause misclassification at inference time or to poison the feedback corpus when feedback is logged for retraining | No framework requires adversarial-robustness testing for deployed models or adversarial-input detection at the serving layer; AI RMF MEASURE-2.5 recommends but does not require |
122
122
  | AML.T0017 | Discover ML Model Ontology (Probe, Extract System Prompt, Map Filters) | Model registry exposure — adversary maps deployed model family, extracts metadata, infers training corpus, harvests prompts and guardrails | No framework requires model-registry RBAC at the granularity needed (per-project read scoping, signed registry queries, audit of model-extraction-pattern queries) |
123
123
  | T1195.001 | Supply Chain Compromise: Software Dependencies and Development Tools | Training pipeline dependency chain — Python wheels, CUDA drivers, ML framework versions, notebook kernels | SCA detects known-vulnerable; XZ-class novel compromise is not detectable without SLSA L3 + reproducible builds for the training environment |
@@ -188,7 +188,7 @@ Descriptions sourced verbatim from `data/atlas-ttps.json` (ATLAS v2026.07, relea
188
188
 
189
189
  | ATLAS ID | ATLAS Name | RAG Attack Class | Control Gap That Lets It Land | Controls That Partially Cover It |
190
190
  |---|---|---|---|---|
191
- | AML.T0020 | Poison Training Data (incl. sub-technique AML.T0020.002 — RAG Knowledge Base Poisoning) | Vector store poisoning (Attack Class 2): adversary injects malicious documents into the retrieval corpus — either behavioral instructions or false facts | Data integrity controls (SI-7, SI-12) are designed for traditional structured data. No framework requires integrity monitoring of vector store contents, embedding distribution shift detection, or hash-based verification of knowledge-base documents. Ingestion pipelines accept unstructured content by design. | NIST-800-53-SI-7, NIST-800-53-SI-12 (both partial — neither covers embedding-space integrity); ALL-AI-PIPELINE-INTEGRITY (universal gap, no framework has the control) |
191
+ | AML.T0020 | Poison Training Data — ATLAS carries no sub-technique for the RAG knowledge-base variant, which this skill treats as its own attack class | Vector store poisoning (Attack Class 2): adversary injects malicious documents into the retrieval corpus — either behavioral instructions or false facts | Data integrity controls (SI-7, SI-12) are designed for traditional structured data. No framework requires integrity monitoring of vector store contents, embedding distribution shift detection, or hash-based verification of knowledge-base documents. Ingestion pipelines accept unstructured content by design. | NIST-800-53-SI-7, NIST-800-53-SI-12 (both partial — neither covers embedding-space integrity); ALL-AI-PIPELINE-INTEGRITY (universal gap, no framework has the control) |
192
192
  | AML.T0043 | Craft Adversarial Data | Embedding manipulation for data exfiltration (Attack Class 1): adversary crafts queries whose embeddings land near sensitive document embeddings, forcing retrieval | No framework requires adversarial-robustness testing for retrieval engines. SI-3 (malicious code protection) does not contemplate adversarial inputs to embedding models. Access control models (AC-3) operate on identified resources, not embedding-similarity-fragments. | NIST-800-53-SI-3, NIST-AI-RMF-MEASURE-2.5 (both partial — neither covers retrieval-engine adversarial robustness) |
193
193
  | AML.T0051 | LLM Prompt Injection (incl. AML.T0051.001 — Indirect Prompt Injection) | Indirect prompt injection via retrieved documents (Attack Class 5): adversarial instructions stored in the knowledge base execute in the LLM's context when retrieved | No framework has a control for prompt injection as an access control failure. The AI agent's service account is properly authorized — AC-2's perspective sees the access as legitimate. ATLAS documents the technique; no framework implements controls. Universal gap `ALL-PROMPT-INJECTION-ACCESS-CONTROL` is open. | NIST-800-53-AC-2 (partial — does not surface model-mediated unauthorized action); ISO-27001-2022-A.8.28 (partial — secure coding scope does not include semantic vulnerabilities); ALL-PROMPT-INJECTION-ACCESS-CONTROL (universal gap) |
194
194
  | AML.T0054 | LLM Jailbreak / Craft Adversarial Data — NLP | Retrieval filter bypass (Attack Class 4) and chunking exploitation (Attack Class 3): semantic border crossing, namespace confusion, split-and-reassemble of sensitive content across chunks | No framework requires safety-guardrail testing for retrieval-augmented systems. NIST AI RMF recommends adversarial testing but does not require it. Filter-application-order (pre-similarity vs. post-similarity) is not addressed by any control. | NIST-AI-RMF-GOVERN-1.7 (partial — recommends but does not require red-team testing); NIST-800-53-SI-12 (partial — retention only, not retrieval scope) |
@@ -127,7 +127,7 @@ Cross-cutting gap: **no security framework treats the four ransomware-specific d
127
127
  | **T1078** | Valid Accounts | Initial access via credential reuse from infostealer markets; AD privilege chain mapping pre-encryption; lateral movement via valid accounts to broaden encryption scope. | Identification: anomalous sign-in UEBA, impossible-travel, infostealer-market evidence. Containment: account disable + session revocation + MFA re-enrollment. Eradication: krbtgt double-rotation, OAuth-grant audit, AD admin-group review. |
128
128
  | **T1059** | Command and Scripting Interpreter | Living-off-the-land via PowerShell, WMI, PsExec; Cobalt Strike Beacon / Sliver / Brute Ratel as C2 framework. | Identification: EDR script-block-logging, suspicious WMI invocations, JA3 fingerprints. Containment: EDR quarantine, egress block to C2 destinations. Eradication: artifact removal, persistence-mechanism cleanup. |
129
129
 
130
- Shadow Copy deletion and exfil-staging via Web Service align to the parent IR playbook's `T1486` and `T1567` entries; the parent's `AML.T0096 / T0017 / T0051` entries do not apply to ransomware-as-a-class but may apply if AI-system data is exfiltrated within the ransomware operation.
130
+ Shadow Copy deletion and exfil-staging via Web Service align to the parent IR playbook's `T1486` and `T1567` entries; the parent's `AML.T0096 / AML.T0017 / AML.T0051` entries do not apply to ransomware-as-a-class but may apply if AI-system data is exfiltrated within the ransomware operation.
131
131
 
132
132
  ATLAS pinned to v2026.07 (August 2026). ATT&CK pinned to v19.2 (August 2026). Both are explicit version pins — never silently upgraded.
133
133