@blamejs/exceptd-skills 0.20.1 → 0.21.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/data/_indexes/_meta.json +47 -47
  3. package/data/_indexes/activity-feed.json +19 -19
  4. package/data/_indexes/catalog-summaries.json +3 -3
  5. package/data/_indexes/chains.json +2 -2
  6. package/data/_indexes/did-ladders.json +1 -1
  7. package/data/_indexes/frequency.json +122 -76
  8. package/data/_indexes/jurisdiction-clocks.json +2 -2
  9. package/data/_indexes/section-offsets.json +654 -654
  10. package/data/_indexes/summary-cards.json +34 -28
  11. package/data/_indexes/theater-fingerprints.json +1 -1
  12. package/data/_indexes/token-budget.json +241 -241
  13. package/data/_indexes/xref.json +34 -14
  14. package/data/atlas-ttps.json +246 -292
  15. package/data/cve-catalog.json +239 -244
  16. package/data/d3fend-catalog.json +2 -3
  17. package/data/framework-control-gaps.json +454 -293
  18. package/data/global-frameworks.json +12 -12
  19. package/data/playbooks/ai-api.json +5 -5
  20. package/data/playbooks/ai-discovered-cve-triage.json +7 -7
  21. package/data/playbooks/cicd-pipeline-compromise.json +1 -2
  22. package/data/playbooks/cloud-iam-incident.json +11 -11
  23. package/data/playbooks/containers.json +3 -3
  24. package/data/playbooks/cred-stores.json +4 -4
  25. package/data/playbooks/crypto-codebase.json +1 -1
  26. package/data/playbooks/crypto.json +4 -4
  27. package/data/playbooks/hardening.json +3 -3
  28. package/data/playbooks/identity-sso-compromise.json +4 -3
  29. package/data/playbooks/idp-incident.json +14 -14
  30. package/data/playbooks/kernel.json +3 -3
  31. package/data/playbooks/llm-tool-use-exfil.json +1 -1
  32. package/data/playbooks/mcp.json +4 -4
  33. package/data/playbooks/post-quantum-migration.json +10 -10
  34. package/data/playbooks/ransomware.json +3 -3
  35. package/data/playbooks/runtime.json +3 -3
  36. package/data/playbooks/sbom.json +3 -3
  37. package/data/playbooks/secrets.json +7 -7
  38. package/data/playbooks/supply-chain-recovery.json +1 -2
  39. package/data/playbooks/webhook-callback-abuse.json +1 -1
  40. package/data/zeroday-lessons.json +391 -396
  41. package/lib/ttp-mapper.js +1 -1
  42. package/manifest-snapshot.json +23 -17
  43. package/manifest-snapshot.sha256 +1 -1
  44. package/manifest.json +126 -120
  45. package/package.json +1 -1
  46. package/sbom.cdx.json +179 -164
  47. package/scripts/backfill-theater-test.js +10 -10
  48. package/scripts/builders/did-ladders.js +1 -1
  49. package/scripts/builders/theater-fingerprints.js +1 -1
  50. package/scripts/check-atlas-catalog-currency.js +47 -4
  51. package/scripts/check-ism-control-references.js +221 -0
  52. package/scripts/check-test-count.js +9 -3
  53. package/scripts/predeploy.js +9 -0
  54. package/scripts/refresh-upstream-catalogs.js +9 -2
  55. package/scripts/release.js +6 -6
  56. package/scripts/sync-manifest-metadata.js +6 -6
  57. package/skills/ai-attack-surface/skill.md +11 -9
  58. package/skills/ai-c2-detection/skill.md +5 -6
  59. package/skills/ai-risk-management/skill.md +6 -5
  60. package/skills/api-security/skill.md +8 -8
  61. package/skills/attack-surface-pentest/skill.md +1 -1
  62. package/skills/cloud-iam-incident/skill.md +2 -2
  63. package/skills/cloud-security/skill.md +2 -1
  64. package/skills/compliance-theater/skill.md +4 -4
  65. package/skills/coordinated-vuln-disclosure/skill.md +1 -1
  66. package/skills/decompression-dos/skill.md +1 -1
  67. package/skills/dlp-gap-analysis/skill.md +3 -3
  68. package/skills/exploit-scoring/skill.md +3 -3
  69. package/skills/framework-gap-analysis/skill.md +3 -3
  70. package/skills/global-grc/skill.md +11 -11
  71. package/skills/idp-incident-response/skill.md +2 -2
  72. package/skills/incident-response-playbook/skill.md +18 -17
  73. package/skills/kernel-lpe-triage/skill.md +1 -1
  74. package/skills/log-injection-telemetry/skill.md +1 -1
  75. package/skills/mcp-agent-trust/skill.md +5 -4
  76. package/skills/mlops-security/skill.md +5 -5
  77. package/skills/multitenancy-isolation/skill.md +1 -1
  78. package/skills/policy-exception-gen/skill.md +1 -1
  79. package/skills/pqc-first/skill.md +1 -1
  80. package/skills/privacy-consent-ops/skill.md +1 -1
  81. package/skills/rag-pipeline-security/skill.md +1 -1
  82. package/skills/ransomware-response/skill.md +2 -2
  83. package/skills/researcher/skill.md +3 -3
  84. package/skills/sector-financial/skill.md +4 -4
  85. package/skills/sector-healthcare/skill.md +3 -2
  86. package/skills/sector-telecom/skill.md +8 -8
  87. package/skills/security-maturity-tiers/skill.md +8 -8
  88. package/skills/self-update-integrity/skill.md +1 -1
  89. package/skills/skill-update-loop/skill.md +3 -3
  90. package/skills/threat-model-currency/skill.md +6 -6
  91. package/skills/threat-modeling-methodology/skill.md +4 -4
  92. package/skills/zeroday-gap-learn/skill.md +3 -3
@@ -598,10 +598,10 @@ const TESTS = {
598
598
  evidence_required: ["tabletop execution log", "FCC notification draft", "cross-jurisdiction timing matrix"],
599
599
  verdict_when_failed: PAPER
600
600
  },
601
- 'AU-ISM-1556': {
602
- claim: "Our telecom posture satisfies AU ISM control 1556 for signaling-protocol abuse.",
603
- test: "Confirm signaling-anomaly baselines per PLMN-pair, gNB firmware hash attestation, and LI-gateway audit. Pull the past 90 days of signaling alerts; confirm triage records. Theater verdict if any of those streams is missing, or if signaling anomalies are unmonitored.",
604
- evidence_required: ["signaling baseline document with PLMN-pair coverage", "gNB attestation telemetry", "alert-triage records"],
601
+ 'AU-ISM-1173-Telecom-NMS': {
602
+ claim: "Privileged users of our telecom network management systems authenticate with multi-factor authentication, which satisfies ISM-1173.",
603
+ test: "List every administrative path into the network management system, element managers, operations support systems and lawful-intercept gateways, including OEM remote-support tunnels. For each, confirm the factor in use and whether it is phishing-resistant, and confirm no credential for it is shared through a mailbox or ticket. Theater verdict if any path accepts a password alone, if lawful-intercept operator accounts are not classified as privileged, or if a support tunnel's credential is shared.",
604
+ evidence_required: ["inventory of administrative paths with the authentication factor used on each", "privileged-role classification covering lawful-intercept gateway operators", "OEM remote-support credential handling record"],
605
605
  verdict_when_failed: PAPER
606
606
  },
607
607
  'GSMA-NESAS-Deployment': {
@@ -636,10 +636,10 @@ const TESTS = {
636
636
  evidence_required: ["IdP tenant inventory", "off-boarding/dormant alerting configuration", "claim-transformation review cadence document"],
637
637
  verdict_when_failed: PAPER
638
638
  },
639
- 'AU-ISM-1559-IdP': {
640
- claim: "Our IdP posture satisfies AU ISM 1559 for identity provider security.",
641
- test: "Confirm IdP token-signing certificate rotation alerting, claim-transformation change-control, management-API token TTL/scope/source-IP enforcement. Confirm the IdP is treated as critical-infrastructure-tier in the asset inventory. Theater verdict if IdP is in 'IT vendor' tier rather than critical-infrastructure tier.",
642
- evidence_required: ["IdP control-plane monitoring rule export", "asset-tier classification record", "management-API token inventory"],
639
+ 'AU-ISM-1685-IdP': {
640
+ claim: "Our identity provider's privileged and service credentials are managed, which satisfies ISM-1685.",
641
+ test: "Confirm alerting on token-signing certificate rotation, change control on claim transformations, and enforced lifetime, scope and source-address limits on management-API tokens. Confirm the identity provider is classified in the critical-infrastructure tier of the asset inventory. Theater verdict if it is classified as an ordinary IT vendor, or if management-API tokens have no lifetime or scope limit.",
642
+ evidence_required: ["identity provider control-plane monitoring rule export", "asset-tier classification record", "management-API token inventory with lifetime and scope"],
643
643
  verdict_when_failed: PAPER
644
644
  },
645
645
  'OFAC-Sanctions-Threat-Actor-Negotiation': {
@@ -666,8 +666,8 @@ const TESTS = {
666
666
  evidence_required: ["cross-account assume-role policy sample", "monitoring rule for chain depth", "external-ID enforcement evidence"],
667
667
  verdict_when_failed: PAPER
668
668
  },
669
- 'AU-ISM-1546-Cloud-Service-Account': {
670
- claim: "Our cloud service-account posture satisfies AU ISM 1546.",
669
+ 'AU-ISM-1685-Cloud-Service-Account': {
670
+ claim: "Our cloud service-account credentials are managed, which satisfies ISM-1685.",
671
671
  test: "Inventory cloud service accounts. Confirm short-lived OIDC tokens (workload identity federation) are used in preference to static keys; for any remaining static keys, confirm rotation policy ≤90 days and source-IP allowlisting. Theater verdict if static keys exist without rotation/IP-allowlisting, or if workload identity federation is available but not adopted.",
672
672
  evidence_required: ["cloud service-account inventory by auth method", "rotation policy document", "source-IP allowlist configuration"],
673
673
  verdict_when_failed: PAPER
@@ -86,7 +86,7 @@ const LADDERS = [
86
86
  {
87
87
  id: "model-exfiltration",
88
88
  attack_class: "Model Weight / Training Data Exfiltration",
89
- primary_ttps: ["AML.T0043", "AML.T0017"],
89
+ primary_ttps: ["AML.T0043", "AML.T0025"],
90
90
  layers: [
91
91
  { layer: "Storage", control: "Model weights in encrypted-at-rest object store; key access via KMS audit", source_skill: "mlops-security", d3fend: ["D3-FE", "D3-MENCR"] },
92
92
  { layer: "Access Control", control: "ML-engineer workstations get read-only via signed checkouts; production weights gated", source_skill: "identity-assurance", d3fend: ["D3-CBAN", "D3-MFA"] },
@@ -59,7 +59,7 @@ const PATTERN_CONTROL_MAP = {
59
59
  { framework: "NIST 800-53", control_id: "IR-4", note: "Incident handling — phases defined but not AI-class triggers" },
60
60
  { framework: "ISO 27001:2022", control_id: "A.5.24-A.5.28", note: "IR planning/preparation/reporting/response/learning" },
61
61
  ],
62
- evidence: { campaign: "SesameOp", rationale: "AML.T0096 LLM Integration Abuse as C2 — no detection triggers exist, so IR procedures have no input" },
62
+ evidence: { campaign: "SesameOp", rationale: "AML.T0096 AI Service API as C2 — no detection triggers exist, so IR procedures have no input" },
63
63
  ttps: ["AML.T0020", "AML.T0096", "AML.T0010"],
64
64
  fast_test: "Search IR playbooks for 'prompt injection', 'model poisoning', 'AI agent', 'LLM', 'MCP server'. Zero matches = THEATER FLAG.",
65
65
  },
@@ -8,15 +8,18 @@
8
8
  * MITRE. An id can therefore carry a name MITRE gives to a different technique,
9
9
  * or name a sub-technique that does not exist, and every gate stays green.
10
10
  *
11
- * Three things are compared, for the release named in `_meta.atlas_version`:
11
+ * Four things are compared, for the release named in `_meta.atlas_version`:
12
12
  * 1. every AML id in the catalog exists upstream
13
13
  * 2. every catalog name is the name upstream gives that id
14
14
  * 3. every AML sub-technique id named inside a `subtechniques` list exists
15
+ * 4. every tactic an entry lists is one upstream assigns to that technique; a
16
+ * sub-technique with no tactics of its own is checked against its parent's
15
17
  *
16
18
  * Divergences recorded in tests/.atlas-divergence-baseline.json are reported and
17
19
  * allowed, so known work in progress does not block a release while any NEW
18
20
  * divergence does. An entry that has been repaired and is still in the baseline
19
- * is also an error, so the file shrinks instead of going stale.
21
+ * is also an error, so the file shrinks instead of going stale. Tactics have no
22
+ * baseline, so a wrong tactic always fails.
20
23
  *
21
24
  * Two naming conventions are accepted without a baseline entry, because they
22
25
  * render the same technique rather than a different one: a sub-technique may be
@@ -129,6 +132,38 @@ function parentNameOf(id, upstream) {
129
132
  return m ? upstream.get(m[1]) || null : null;
130
133
  }
131
134
 
135
+ // Format 6 records each technique's tactics as `achieves` relationships in the
136
+ // top-level `relationships` section. Returns technique id -> set of tactic names.
137
+ function parseTactics(text, names) {
138
+ const out = new Map();
139
+ const at = text.indexOf("\nrelationships:");
140
+ if (at < 0) return out;
141
+ const re = /- source: (AML\.T[0-9.]+)\n\s+target: (AML\.TA[0-9]+)\n\s+relationship-type: achieves\b/g;
142
+ const rel = text.slice(at);
143
+ let m;
144
+ while ((m = re.exec(rel)) !== null) {
145
+ if (!out.has(m[1])) out.set(m[1], new Set());
146
+ out.get(m[1]).add(names.get(m[2]) || m[2]);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ // The tactic values an entry lists that upstream does not assign to it. An
152
+ // entry may list fewer tactics than upstream; it may not list a different one.
153
+ // A sub-technique with no tactics of its own is checked against its parent's,
154
+ // and an id upstream assigns no tactic to is not checked.
155
+ function tacticProblems(id, tactic, tactics) {
156
+ const values = (Array.isArray(tactic) ? tactic : [tactic])
157
+ .map((t) => String(t == null ? "" : t).trim())
158
+ .filter(Boolean);
159
+ if (!values.length) return [];
160
+ const parent = /^(AML\.T\d+)\.\d+$/.exec(id);
161
+ const allowed = (tactics.get(id) && tactics.get(id).size ? tactics.get(id) : null) ||
162
+ (parent ? tactics.get(parent[1]) : null);
163
+ if (!allowed || !allowed.size) return [];
164
+ return values.filter((v) => !allowed.has(v)).map((v) => ({ id, ours: v, theirs: [...allowed].sort() }));
165
+ }
166
+
132
167
  function readBaseline() {
133
168
  if (!fs.existsSync(BASELINE)) return { names: {}, missing_ids: [], missing_subtechniques: [] };
134
169
  try {
@@ -194,6 +229,13 @@ async function main() {
194
229
  }
195
230
  }
196
231
 
232
+ const upstreamTactics = parseTactics(release.text, upstream);
233
+ const wrongTactic = [];
234
+ for (const id of ids) {
235
+ if (!AML_ID.test(id) || !upstream.has(id)) continue;
236
+ wrongTactic.push(...tacticProblems(id, catalog[id].tactic, upstreamTactics));
237
+ }
238
+
197
239
  // Each allowance covers exactly one finding. A recorded name does not excuse a
198
240
  // sub-technique id, and a recorded sub-technique id does not excuse its
199
241
  // siblings, so a new bad id under an entry that already diverges still fails.
@@ -218,6 +260,7 @@ async function main() {
218
260
  for (const x of newMisnamed) problems.push(`${x.id} is named ${JSON.stringify(x.ours)}; ATLAS ${pin} names it ${JSON.stringify(x.theirs)}`);
219
261
  for (const x of newMissing) problems.push(`${x.id} (${JSON.stringify(x.ours)}) does not exist in ATLAS ${pin}`);
220
262
  for (const x of newMissingSub) problems.push(`${x.id} names sub-technique ${x.sub}, which does not exist in ATLAS ${pin}`);
263
+ for (const x of wrongTactic) problems.push(`${x.id} lists tactic ${JSON.stringify(x.ours)}; ATLAS ${pin} assigns ${x.theirs.map((t) => JSON.stringify(t)).join(", ")}`);
221
264
  for (const id of staleNames) problems.push(`${id} is recorded as a name divergence but now agrees with upstream; remove it from ${path.relative(ROOT, BASELINE)}`);
222
265
  for (const id of staleMissing) problems.push(`${id} is recorded as absent upstream but now resolves; remove it from ${path.relative(ROOT, BASELINE)}`);
223
266
  for (const sub of staleSubs) problems.push(`${sub} is recorded as absent upstream but is no longer named or now resolves; remove it from ${path.relative(ROOT, BASELINE)}`);
@@ -241,7 +284,7 @@ async function main() {
241
284
  return;
242
285
  }
243
286
 
244
- emit(`[check-atlas-catalog-currency] PASS — every id and name agrees with ATLAS ${pin}, or is a recorded divergence`);
287
+ emit(`[check-atlas-catalog-currency] PASS — every id, name and tactic agrees with ATLAS ${pin}, or is a recorded divergence`);
245
288
  }
246
289
 
247
290
  if (require.main === module) {
@@ -253,4 +296,4 @@ if (require.main === module) {
253
296
 
254
297
  // Only the pieces that carry judgment are exported; `main` is the CLI and is
255
298
  // reached through the guard above.
256
- module.exports = { parseNames, isRenderingOf, parentNameOf };
299
+ module.exports = { parseNames, isRenderingOf, parentNameOf, parseTactics, tacticProblems };
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Checks every Australian ISM control the repository cites against the ISM
5
+ * release pinned in sources/index.json (`au_frameworks.ism.oscal_release`).
6
+ *
7
+ * Two things are compared:
8
+ * 1. every ISM-NNNN token in the shipped data, skills, library and scripts
9
+ * names a control that exists in the release
10
+ * 2. every framework-control-gaps key of the form AU-ISM-NNNN carries
11
+ * control_id ISM-NNNN, and its control_name begins with that control's
12
+ * statement, so the key cannot describe a different control
13
+ *
14
+ * The release is ASD's own OSCAL catalog, fetched from
15
+ * github.com/AustralianCyberSecurityCentre/ism-oscal at the pinned tag and
16
+ * cached under .cache/upstream/ism/. With no cache and no network the check
17
+ * reports that it could not verify and exits 2.
18
+ *
19
+ * Usage: node scripts/check-ism-control-references.js [--json]
20
+ */
21
+
22
+ const fs = require("node:fs");
23
+ const path = require("node:path");
24
+ const https = require("node:https");
25
+
26
+ const ROOT = path.resolve(__dirname, "..");
27
+ const SOURCES = path.join(ROOT, "sources", "index.json");
28
+ const REGISTRY = path.join(ROOT, "data", "framework-control-gaps.json");
29
+ const CACHE_DIR = path.join(ROOT, ".cache", "upstream", "ism");
30
+ const JSON_OUT = process.argv.includes("--json");
31
+
32
+ // Paths scanned for citations, relative to the repository root. CHANGELOG.md
33
+ // records past releases and is not scanned.
34
+ const SCAN_ROOTS = ["data", "skills", "lib", "scripts", "AGENTS.md", "README.md", "ARCHITECTURE.md"];
35
+ const SKIP_DIRS = new Set(["_indexes", "node_modules", "vendor"]);
36
+ const SELF = path.relative(ROOT, __filename).split(path.sep).join("/");
37
+
38
+ const PIN_SHAPE = /^v[0-9]{4}\.[0-9]{2}\.[0-9]+$/;
39
+ // "ISM-" must not follow a letter or digit, so AU-ISM-1546 is read as a
40
+ // citation and a name such as PRISM-1234 is not.
41
+ const TOKEN = /(?<![A-Za-z0-9])ISM-([0-9]{4})\b/g;
42
+ const REGISTRY_KEY = /^AU-ISM-([0-9]{4})(?:-|$)/;
43
+
44
+ function emit(line) {
45
+ if (!JSON_OUT) process.stdout.write(line + "\n");
46
+ }
47
+
48
+ function fetchText(url, redirects = 0) {
49
+ return new Promise((resolve, reject) => {
50
+ if (redirects > 5) return reject(new Error("too many redirects"));
51
+ const req = https.get(url, { headers: { "user-agent": "exceptd-ism-references" } }, (res) => {
52
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
53
+ res.resume();
54
+ return resolve(fetchText(res.headers.location, redirects + 1));
55
+ }
56
+ if (res.statusCode !== 200) {
57
+ res.resume();
58
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
59
+ }
60
+ let body = "";
61
+ res.setEncoding("utf8");
62
+ res.on("data", (c) => { body += c; });
63
+ res.on("end", () => resolve(body));
64
+ });
65
+ req.setTimeout(60000, () => req.destroy(new Error("timed out")));
66
+ req.on("error", reject);
67
+ });
68
+ }
69
+
70
+ async function loadRelease(pin) {
71
+ if (!PIN_SHAPE.test(pin)) throw new Error(`oscal_release ${JSON.stringify(pin)} is not a vYYYY.MM.N tag`);
72
+ const cached = path.join(CACHE_DIR, `ISM_catalog-${pin}.json`);
73
+ let text = null;
74
+ try { text = fs.readFileSync(cached, "utf8"); }
75
+ catch (e) { if (e.code !== "ENOENT") throw e; }
76
+ let source = "cache";
77
+ if (text === null) {
78
+ text = await fetchText(`https://raw.githubusercontent.com/AustralianCyberSecurityCentre/ism-oscal/${pin}/ISM_catalog.json`);
79
+ source = "network";
80
+ }
81
+ const controls = parseControls(text);
82
+ const version = (JSON.parse(text).catalog.metadata || {}).version;
83
+ if (`v${version}` !== pin) throw new Error(`the catalog for ${pin} declares version ${JSON.stringify(version)}`);
84
+ if (source === "network") {
85
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
86
+ fs.writeFileSync(cached, text);
87
+ }
88
+ return { controls, source };
89
+ }
90
+
91
+ // Map of "ISM-NNNN" to the control's statement, from an OSCAL catalog document.
92
+ function parseControls(text) {
93
+ const out = new Map();
94
+ (function walk(g) {
95
+ for (const x of g.controls || []) {
96
+ const m = /^ism-([0-9]{4})$/.exec(x.id || "");
97
+ if (m) {
98
+ const st = (x.parts || []).find((p) => p.name === "statement");
99
+ out.set(`ISM-${m[1]}`, ((st && st.prose) || "").replace(/\s+/g, " ").trim());
100
+ }
101
+ walk(x);
102
+ }
103
+ for (const y of g.groups || []) walk(y);
104
+ })(JSON.parse(text).catalog || {});
105
+ return out;
106
+ }
107
+
108
+ // Every ISM-NNNN occurrence in a text, with its 1-based line number.
109
+ function findTokens(text) {
110
+ const out = [];
111
+ const lines = text.split("\n");
112
+ lines.forEach((line, i) => {
113
+ let m;
114
+ TOKEN.lastIndex = 0;
115
+ while ((m = TOKEN.exec(line)) !== null) out.push({ id: `ISM-${m[1]}`, line: i + 1 });
116
+ });
117
+ return out;
118
+ }
119
+
120
+ // A registry entry keyed AU-ISM-NNNN must name ISM-NNNN and describe it. The
121
+ // control_name may add a scope after the statement, as in
122
+ // "... managed (identity provider tenant)".
123
+ function registryProblems(key, entry, controls) {
124
+ const m = REGISTRY_KEY.exec(key);
125
+ if (!m) return [];
126
+ const id = `ISM-${m[1]}`;
127
+ const problems = [];
128
+ if (!controls.has(id)) {
129
+ problems.push(`${key}: ${id} does not exist in the pinned release`);
130
+ return problems;
131
+ }
132
+ if (entry.control_id !== id) problems.push(`${key}: control_id is ${JSON.stringify(entry.control_id)}, expected ${id}`);
133
+ const statement = controls.get(id).replace(/\.$/, "");
134
+ const name = String(entry.control_name || "");
135
+ if (!name.startsWith(statement)) {
136
+ problems.push(`${key}: control_name ${JSON.stringify(name)} does not begin with the statement of ${id}: ${JSON.stringify(statement)}`);
137
+ }
138
+ return problems;
139
+ }
140
+
141
+ function scanFiles() {
142
+ const files = [];
143
+ function walk(p) {
144
+ let st;
145
+ try { st = fs.statSync(p); } catch (e) { if (e.code === "ENOENT") return; throw e; }
146
+ if (st.isDirectory()) {
147
+ if (SKIP_DIRS.has(path.basename(p))) return;
148
+ for (const e of fs.readdirSync(p)) walk(path.join(p, e));
149
+ } else if (/\.(json|md|js)$/.test(p)) {
150
+ files.push(p);
151
+ }
152
+ }
153
+ for (const r of SCAN_ROOTS) walk(path.join(ROOT, r));
154
+ return files;
155
+ }
156
+
157
+ async function main() {
158
+ const sources = JSON.parse(fs.readFileSync(SOURCES, "utf8"));
159
+ const ism = ((sources.sources || sources).au_frameworks || {}).ism || {};
160
+ const pin = ism.oscal_release;
161
+ if (!pin) {
162
+ process.stderr.write("[check-ism-control-references] sources/index.json has no au_frameworks.ism.oscal_release\n");
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+
167
+ let release;
168
+ try {
169
+ release = await loadRelease(pin);
170
+ } catch (e) {
171
+ process.stderr.write(
172
+ `[check-ism-control-references] COULD NOT VERIFY: ${e.message}\n` +
173
+ "No citation was compared to anything, which is not the same as passing. " +
174
+ `Re-run with network access, or place ISM_catalog-${pin}.json under ${path.relative(ROOT, CACHE_DIR)}.\n`
175
+ );
176
+ process.exitCode = 2;
177
+ return;
178
+ }
179
+ emit(`[check-ism-control-references] ISM ${pin} (${release.source}): ${release.controls.size} controls`);
180
+
181
+ const problems = [];
182
+ let citations = 0;
183
+ for (const f of scanFiles()) {
184
+ const rel = path.relative(ROOT, f).split(path.sep).join("/");
185
+ if (rel === SELF) continue;
186
+ for (const t of findTokens(fs.readFileSync(f, "utf8"))) {
187
+ citations++;
188
+ if (!release.controls.has(t.id)) problems.push(`${rel}:${t.line}: ${t.id} does not exist in ISM ${pin}`);
189
+ }
190
+ }
191
+
192
+ const registry = JSON.parse(fs.readFileSync(REGISTRY, "utf8"));
193
+ let keys = 0;
194
+ for (const [key, entry] of Object.entries(registry)) {
195
+ if (key.startsWith("_") || !REGISTRY_KEY.test(key)) continue;
196
+ keys++;
197
+ problems.push(...registryProblems(key, entry, release.controls));
198
+ }
199
+
200
+ emit(` citations checked: ${citations} AU-ISM registry keys checked: ${keys}`);
201
+
202
+ if (JSON_OUT) {
203
+ process.stdout.write(JSON.stringify({ ok: problems.length === 0, pin, source: release.source, citations, registry_keys: keys, problems }, null, 2) + "\n");
204
+ }
205
+ if (problems.length) {
206
+ process.stderr.write(`[check-ism-control-references] FAIL: ${problems.length} problem(s) against ISM ${pin}:\n`);
207
+ for (const p of problems) process.stderr.write(` - ${p}\n`);
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ emit(`[check-ism-control-references] PASS: every ISM control cited exists in ${pin}, and every AU-ISM registry key describes the control it names`);
212
+ }
213
+
214
+ if (require.main === module) {
215
+ main().catch((e) => {
216
+ process.stderr.write(`[check-ism-control-references] ${e.stack || e.message}\n`);
217
+ process.exitCode = 2;
218
+ });
219
+ }
220
+
221
+ module.exports = { parseControls, findTokens, registryProblems };
@@ -6,8 +6,11 @@
6
6
  * diff-coverage gates cannot see.
7
7
  *
8
8
  * Counts DECLARATIONS statically across `tests/*.test.js` — `test(` and `it(`
9
- * with their `.only` / `.skip` variants. `describe(` is NOT counted: a container
10
- * is not a test. Blind spot: a test neutered in place still counts as one.
9
+ * with their `.only` variants. `describe(` is NOT counted: a container is not
10
+ * a test. `test.skip(`, and a declaration whose options on the same line set
11
+ * `skip` to `true` or to a string, are not counted, so a test disabled in place
12
+ * reads as a lost test. A conditional skip such as `{ skip: !HAS_KEY }` still
13
+ * counts, and so does a skip set on a later line of a multi-line options object.
11
14
  *
12
15
  * exit 0 at or above baseline minus tolerance, 1 when it drops further, 2 when
13
16
  * the baseline file is missing or malformed.
@@ -48,7 +51,10 @@ function countTests(filePath) {
48
51
  // Drop a trailing line comment too (`test('x'); // disabled`).
49
52
  const stripped = noStrings.replace(/\/\/.*$/, '').trim();
50
53
  if (!stripped) continue;
51
- if (/(?<![A-Za-z0-9_$.])(?:test|it)(?:\.only|\.skip)?\s*\(/.test(stripped)) count++;
54
+ if (!/(?<![A-Za-z0-9_$.])(?:test|it)(?:\.only)?\s*\(/.test(stripped)) continue;
55
+ // String literals are blanked to '' above, so `skip: 'reason'` reads as `skip: ''`.
56
+ if (/\bskip\s*:\s*(?:true\b|''\s*[,}])/.test(stripped)) continue;
57
+ count++;
52
58
  }
53
59
  return count;
54
60
  }
@@ -209,6 +209,15 @@ const GATES = [
209
209
  args: [path.join(ROOT, "scripts", "check-atlas-catalog-currency.js")],
210
210
  ciJobName: "Data integrity (catalog + manifest snapshot)",
211
211
  },
212
+ {
213
+ // Every ISM-NNNN citation must name a control in the pinned ism-oscal
214
+ // release, and every AU-ISM-NNNN registry key must describe that control.
215
+ // Fetches the release catalog on a cold cache; a tag is immutable.
216
+ name: "ISM control references (citations vs. the pinned ISM release)",
217
+ command: process.execPath,
218
+ args: [path.join(ROOT, "scripts", "check-ism-control-references.js")],
219
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
220
+ },
212
221
  ];
213
222
 
214
223
  function runGate(gate) {
@@ -537,14 +537,21 @@ async function refreshIcsAttack({ dry = false, cap = Infinity, _deps = {} } = {}
537
537
 
538
538
  const ATLAS_SRC = "https://raw.githubusercontent.com/mitre-atlas/atlas-navigator-data/main/dist/stix-atlas.json";
539
539
 
540
+ // The STIX mirror trails ATLAS releases, so a phase can arrive under a slug
541
+ // ATLAS has since renamed. Every slug a release has used for a tactic maps to
542
+ // that tactic's current name: AML.TA0000 is AI Model Access and AML.TA0001 is
543
+ // AI Attack Adaptation.
540
544
  function atlasTactic(phases) {
541
545
  const map = {
542
546
  "reconnaissance": "Reconnaissance", "resource-development": "Resource Development",
543
- "initial-access": "Initial Access", "ml-model-access": "AI Model Access",
547
+ "initial-access": "Initial Access",
548
+ "ml-model-access": "AI Model Access", "ai-model-access": "AI Model Access",
544
549
  "execution": "Execution", "persistence": "Persistence",
545
550
  "privilege-escalation": "Privilege Escalation", "defense-evasion": "Defense Evasion",
546
551
  "credential-access": "Credential Access", "discovery": "Discovery",
547
- "collection": "Collection", "ml-attack-staging": "AI Attack Staging",
552
+ "lateral-movement": "Lateral Movement", "collection": "Collection",
553
+ "ml-attack-staging": "AI Attack Adaptation", "ai-attack-staging": "AI Attack Adaptation",
554
+ "ai-attack-adaptation": "AI Attack Adaptation",
548
555
  "command-and-control": "Command and Control", "exfiltration": "Exfiltration",
549
556
  "impact": "Impact"
550
557
  };
@@ -563,15 +563,15 @@ function cmdRelease() {
563
563
  _section("verify npm");
564
564
  // npm accepts a publish before the registry serves it ("Your package is being
565
565
  // processed and may take a few minutes"), so `npm view` can report the
566
- // previous version for a minute or two after release.yml succeeds. Poll for
567
- // up to three minutes before reading a mismatch as a failed publish.
566
+ // previous version for several minutes after release.yml succeeds. Poll for
567
+ // up to ten minutes before reading a mismatch as a failed publish.
568
568
  var npmVersion = "";
569
- for (var _n = 0; _n < 18; _n++) {
569
+ for (var _n = 0; _n < 60; _n++) {
570
570
  npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
571
571
  if (npmVersion === next) break;
572
- if (_n < 17) {
572
+ if (_n < 59) {
573
573
  console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") +
574
- ", waiting for " + next + " (" + (_n + 1) + "/18)");
574
+ ", waiting for " + next + " (" + (_n + 1) + "/60)");
575
575
  _spawn(process.execPath, ["-e", "setTimeout(function(){},10000)"], { stdio: "ignore" });
576
576
  }
577
577
  }
@@ -592,7 +592,7 @@ function cmdRelease() {
592
592
  throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
593
593
  }
594
594
 
595
- // The workflow has finished and the registry has had three minutes, so an
595
+ // The workflow has finished and the registry has had ten minutes, so an
596
596
  // empty or mismatched version must not read as a completed release.
597
597
  if (npmVersion !== next) {
598
598
  throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
@@ -6,11 +6,11 @@
6
6
  * Run it whenever frontmatter changes, then re-run sign-all — and, when a
7
7
  * cross-ref array changed, refresh-reverse-refs + build-indexes.
8
8
  *
9
- * `last_threat_review` and `forward_watch` MIRROR frontmatter exactly and sync
10
- * by replace. The cross-reference arrays are an enriched superset — the
11
- * manifest carries curated refs frontmatter does not — so they sync by UNION;
12
- * replacing them drops the curated refs build-indexes and refresh-reverse-refs
13
- * read. `description` is cached in the manifest but is not synced here.
9
+ * `description`, `last_threat_review` and `forward_watch` MIRROR frontmatter
10
+ * exactly and sync by replace. The cross-reference arrays are an enriched
11
+ * superset — the manifest carries curated refs frontmatter does not — so they
12
+ * sync by UNION; replacing them drops the curated refs build-indexes and
13
+ * refresh-reverse-refs read.
14
14
  *
15
15
  * Exit codes: 0 = wrote (or already in sync), 1 = a skill file was missing or
16
16
  * its frontmatter failed to parse.
@@ -23,7 +23,7 @@ const lint = require("../lib/lint-skills.js");
23
23
  const ROOT = path.resolve(__dirname, "..");
24
24
  const MANIFEST = path.join(ROOT, "manifest.json");
25
25
 
26
- const MIRRORED_SCALAR = ["last_threat_review"];
26
+ const MIRRORED_SCALAR = ["description", "last_threat_review"];
27
27
  const MIRRORED_ARRAY = ["forward_watch"];
28
28
  // Union, never replace — a replace drops the manifest's curated refs.
29
29
  const MIRRORED_COVER = ["data_deps", "framework_gaps", "atlas_refs", "attack_refs", "rfc_refs", "cwe_refs", "d3fend_refs"];
@@ -22,6 +22,7 @@ atlas_refs:
22
22
  - AML.T0096
23
23
  - AML.T0016
24
24
  - AML.T0017
25
+ - AML.T0013
25
26
  - AML.T0018
26
27
  attack_refs:
27
28
  - T1566
@@ -96,13 +97,13 @@ This is a supply chain attack surface. Every MCP server a user installs is a pot
96
97
 
97
98
  The implication: the time between a vulnerability's introduction into a codebase and its reliable exploitation has compressed from months or years to hours or days for AI-capable threat actors. Patch management SLAs designed for human-speed exploit development are structurally inadequate.
98
99
 
99
- **ATLAS ref:** AML.T0016 (Obtain Capabilities: Develop Capabilities)
100
+ **ATLAS ref:** AML.T0017 (Develop Capabilities)
100
101
 
101
102
  ### 4. AI Credential Phishing Acceleration
102
103
 
103
104
  Credential theft driven by AI increased 160% in 2025. 82.6% of phishing emails now contain AI-generated content undetectable by grammar/style checks. Traditional phishing detection heuristics (poor grammar, unusual phrasing, template patterns) are no longer reliable detectors.
104
105
 
105
- **ATLAS ref:** AML.T0016 (Obtain Capabilities: Develop Capabilities — misuse of public AI APIs to generate phishing payloads)
106
+ **ATLAS ref:** AML.T0016 (Obtain Capabilities), covering public AI APIs misused to generate phishing payloads
106
107
 
107
108
  ### 5. AI as Covert C2 — SesameOp
108
109
 
@@ -140,7 +141,7 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
140
141
 
141
142
  ### 11. AI-Discovered + AI-Weaponized Supply-Chain Worms
142
143
 
143
- **CVE-2026-45321** — Mini Shai-Hulud TanStack npm worm (CVSS 9.6, ~150M weekly downloads across 42 @tanstack/* packages, CISA KEV pending). Disclosed 2026-05-11. The attack chain — Pwn-Request via `pull_request_target` on TanStack's bundle-size workflow, pnpm-store cache poisoning under the `actions/cache` key, and OIDC-token theft on the next main push — is engineering-grade and weaponizes three independently-benign primitives. While attribution (TeamPCP) records no AI-assisted exploit development for this specific instance, the worm pattern is exactly what AML.T0016-class capability-development now produces at AI cadence: chained CI/CD primitives that no individual component owner recognises as exploitable. Treat the @tanstack/* surface as an exemplar of the broader AML.T0010 (AI Supply Chain Compromise) threat applied to JS toolchains that the AI assistant ecosystem depends on.
144
+ **CVE-2026-45321** — Mini Shai-Hulud TanStack npm worm (CVSS 9.6, ~150M weekly downloads across 42 @tanstack/* packages, CISA KEV pending). Disclosed 2026-05-11. The attack chain — Pwn-Request via `pull_request_target` on TanStack's bundle-size workflow, pnpm-store cache poisoning under the `actions/cache` key, and OIDC-token theft on the next main push — is engineering-grade and weaponizes three independently-benign primitives. While attribution (TeamPCP) records no AI-assisted exploit development for this specific instance, the worm pattern is exactly what AML.T0017-class capability development now produces at AI cadence: chained CI/CD primitives that no individual component owner recognises as exploitable. Treat the @tanstack/* surface as an exemplar of the broader AML.T0010 (AI Supply Chain Compromise) threat applied to JS toolchains that the AI assistant ecosystem depends on.
144
145
 
145
146
  ---
146
147
 
@@ -164,7 +165,7 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
164
165
  | UK NCSC CAF | Principle A4 (Supply Chain), B2 (Identity and Access Control), B4 (System Security), C1 (Security Monitoring) | Outcome-based language. NCSC's 2024 guidance on securing AI systems and the DSIT AI Cyber Code of Practice (2025) name the attack classes at the principle level (prompt injection, model supply chain, monitoring), but the CAF outcome statements are unchanged. An organisation achieves CAF outcomes at Achieved level with zero MCP-trust controls and zero prompt-injection detection. |
165
166
  | UK DSIT AI Cyber Code of Practice (2025) | 13 principles for secure AI development and deployment | Principles-based. Names training-data integrity, model-deployment security, monitoring, and supply chain — but as principles, not testable controls. No technical floor for prompt-injection defence, MCP allowlisting, or AI-as-C2 detection. |
166
167
  | AU ASD Essential 8 | Strategy: Application Control + User Application Hardening + Restrict Administrative Privileges | Application Control (allowlisting) covers what runs locally; it does not cover AI-assistant tool-use chains where the AI agent dynamically composes the executed action. None of the eight strategies address prompt injection, model supply chain, or LLM-as-C2 channel. |
167
- | AU ASD ISM | ISM-1808 (cloud consumer responsibilities) + ISM-1728 (supply chain risk) + ISM-1228 (event monitoring) | ISM-1728 supply-chain control is scoped to traditional vendor classes; LLM APIs and MCP servers fall outside enumerated supply-chain categories. ISM-1228 event-log monitoring assumes signature-or-rule detection over known indicators — AI API egress as C2 and prompt-injection-coerced actions produce no traditional indicator. |
168
+ | AU ASD ISM | ISM-2156, ISM-2157 and ISM-2158 (agentic AI: minimum tools, task-scoped authorization, retrieved content treated as untrusted) + ISM-1924 (prompt injection) + ISM-1452 (supply chain risk assessment) + ISM-1228 (event analysis) | The ISM names agentic AI directly, so the gap is narrower than a missing control. ISM-1452's supplier assessment gives no method for rating an LLM API or MCP server provider, and ISM-1228's event analysis has no indicator for AI API egress used as C2 or for an action a prompt injection coerced, because both produce ordinary, authorized traffic. |
168
169
  | AU Voluntary AI Safety Standard (10 guardrails, 2024) | Guardrails 5 (test and monitor), 6 (transparency), 8 (record-keeping) | Voluntary, principles-language. Operationally identical to the UK DSIT AI Cyber Code lag — names the right concerns at the principle layer without testable controls for prompt injection, MCP trust, or AI-as-C2 detection. |
169
170
  | AU APRA CPS 234 | Para 27 (information security capability proportional to vulnerabilities and threats) + APRA CPG 235 (managing data risk) | "Proportional" capability language. APRA-regulated entities deploying AI assistants meet CPS 234 attestation with traditional information-security capability; AI-specific attack surface is not an examined capability class. APRA Information Paper on AI (2024) names the risks at the supervisory level without altering CPS 234 obligations. |
170
171
 
@@ -176,12 +177,13 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
176
177
  |---|---|---|---|---|
177
178
  | AML.T0054 | LLM Jailbreak | Missing in all major frameworks | No control covers adversarial-instruction injection that bypasses guardrails and coerces the model into attacker-chosen actions | CVE-2025-53773 (GitHub Copilot YOLO-mode RCE) |
178
179
  | AML.T0010 | AI Supply Chain Compromise | Partial (ISO A.8.30) | A.8.30 covers outsourced development; does not cover MCP server trust, package signing for AI tools | CVE-2026-30615 (Windsurf MCP) |
179
- | AML.T0096 | LLM Integration Abuse (C2) | Missing in all major frameworks | No framework has a control for AI API traffic as C2 channel | SesameOp campaign |
180
+ | AML.T0096 | AI Service API (C2) | Missing in all major frameworks | No framework has a control for AI API traffic as C2 channel | SesameOp campaign |
180
181
  | AML.T0020 | Training Data Poisoning | Partial (NIST AI RMF) | NIST AI RMF identifies the risk; no specific technical control | Supply chain logistics model poisoning |
181
182
  | AML.T0043 | Craft Adversarial Data | Partial (SI-10) | SI-10 covers web input validation; not semantic injection in LLM prompts | RAG vector manipulation |
182
183
  | AML.T0051 | LLM Prompt Injection | Missing in all major frameworks | Zero controls in NIST, ISO, SOC 2, PCI for prompt injection | CVE-2025-53773, indirect injection via retrieved docs |
183
- | AML.T0017 | Discover ML Model Ontology | Partial (awareness only) | No framework requires monitoring for adversary mapping of deployed model family, guardrail surface, or system-prompt structure via inference-API probing | Reconnaissance step preceding PROMPTSTEAL-class targeting; AML-model registry exposure |
184
- | AML.T0016 | Obtain Capabilities: Develop Capabilities | Missing (misuse dimension) | Frameworks don't address adversary AI-assisted exploit development or use of public AI APIs to craft malware/phishing payloads | Copy Fail AI discovery (41% of 2025 0-days), PROMPTFLUX, PROMPTSTEAL, phishing generation |
184
+ | AML.T0013 | Discover AI Model Ontology | Partial (awareness only) | No framework requires monitoring for adversary mapping of deployed model family, guardrail surface, or system-prompt structure via inference-API probing | Reconnaissance step preceding PROMPTSTEAL-class targeting; AML-model registry exposure |
185
+ | AML.T0017 | Develop Capabilities | Missing (misuse dimension) | Frameworks don't address adversary AI-assisted exploit development | Copy Fail AI discovery (41% of 2025 0-days) |
186
+ | AML.T0016 | Obtain Capabilities | Missing (misuse dimension) | Frameworks don't address adversary use of public AI APIs to craft malware/phishing payloads | PROMPTFLUX, PROMPTSTEAL, phishing generation |
185
187
  | AML.T0018 | Manipulate AI Model | Partial (NIST AI RMF) | No technical control requirements for model integrity verification | Training pipeline poisoning |
186
188
 
187
189
  ---
@@ -313,11 +315,11 @@ D3FEND v1.3.0+ references from `data/d3fend-catalog.json`. The AI attack surface
313
315
 
314
316
  | D3FEND ID | Name | Layer | Rationale (what it counters here) |
315
317
  |---|---|---|---|
316
- | `D3-IOPR` | Input/Output Profiling | SDK / application | SDK-level prompt and completion inspection — the foundational control for AML.T0051 (prompt injection), AML.T0096 (LLM C2), AML.T0054 (extraction). Without per-call I/O profiling, every detection step below is degraded. |
318
+ | `D3-IOPR` | Input/Output Profiling | SDK / application | SDK-level prompt and completion inspection — the foundational control for AML.T0051 (prompt injection), AML.T0096 (LLM C2), AML.T0054 (jailbreak). Without per-call I/O profiling, every detection step below is degraded. |
317
319
  | `D3-CSPP` | Client-server Payload Profiling | LLM / MCP gateway | Gateway-layer inspection of tool-call args and prompt/completion bodies. Necessary when the AI client (mobile app, browser extension, IDE) cannot host `D3-IOPR` instrumentation in-process — the gateway becomes the only content-aware control. |
318
320
  | `D3-EAL` | Executable Allowlisting | Endpoint / managed host | Restricts which AI-tool binaries (IDE assistants, browser extensions, MCP servers) can execute on managed endpoints. Direct counter to AML.T0010 (ML supply-chain compromise) for tooling shipped as native binaries; precondition for managed-endpoint clipboard- and code-completion controls. |
319
321
  | `D3-FAPA` | File Access Pattern Analysis | Endpoint / data tier | Detects RAG-corpus and training-data abuse by pattern-matching the file-access shape of AML.T0018 (model poisoning at training time) and AML.T0020 (RAG retrieval abuse). Anchors the data-tier defence against poisoning that prompt-layer controls cannot see. |
320
- | `D3-NTA` | Network Traffic Analysis | Network egress | Per-identity baseline of model-API and MCP-server egress. Catches the AML.T0017 capability-development pattern (PROMPTFLUX-style rapid querying) and the AML.T0096 covert-C2 destination shape when SDK instrumentation is partial or missing. |
322
+ | `D3-NTA` | Network Traffic Analysis | Network egress | Per-identity baseline of model-API and MCP-server egress. Catches the AML.T0016 generative-AI misuse pattern (PROMPTFLUX-style rapid querying) and the AML.T0096 covert-C2 destination shape when SDK instrumentation is partial or missing. |
321
323
 
322
324
  **Defense-in-depth posture:** `D3-EAL` is the prerequisite endpoint layer (only sanctioned AI clients run); `D3-FAPA` is the data-tier layer (RAG and training corpora); `D3-IOPR` and `D3-CSPP` are the content-aware application and gateway layers; `D3-NTA` is the network-observability backstop. Skills that recommend a single layer alone are flagged as incomplete during Analysis Procedure Step 7.
323
325