@blamejs/exceptd-skills 0.20.1 → 0.21.1
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 +48 -0
- package/data/_indexes/_meta.json +50 -50
- package/data/_indexes/activity-feed.json +21 -21
- package/data/_indexes/catalog-summaries.json +10 -10
- package/data/_indexes/chains.json +2 -2
- package/data/_indexes/did-ladders.json +1 -1
- package/data/_indexes/frequency.json +122 -76
- package/data/_indexes/jurisdiction-clocks.json +2 -2
- package/data/_indexes/section-offsets.json +654 -654
- package/data/_indexes/summary-cards.json +34 -28
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +241 -241
- package/data/_indexes/xref.json +34 -14
- package/data/atlas-ttps.json +246 -292
- package/data/attack-techniques.json +13 -1
- package/data/cve-catalog.json +833 -246
- package/data/cwe-catalog.json +6 -2
- package/data/d3fend-catalog.json +2 -3
- package/data/framework-control-gaps.json +479 -293
- package/data/global-frameworks.json +12 -12
- package/data/playbooks/ai-api.json +5 -5
- package/data/playbooks/ai-discovered-cve-triage.json +7 -7
- package/data/playbooks/cicd-pipeline-compromise.json +1 -2
- package/data/playbooks/cloud-iam-incident.json +11 -11
- package/data/playbooks/containers.json +3 -3
- package/data/playbooks/cred-stores.json +4 -4
- package/data/playbooks/crypto-codebase.json +1 -1
- package/data/playbooks/crypto.json +4 -4
- package/data/playbooks/hardening.json +3 -3
- package/data/playbooks/identity-sso-compromise.json +4 -3
- package/data/playbooks/idp-incident.json +14 -14
- package/data/playbooks/kernel.json +3 -3
- package/data/playbooks/llm-tool-use-exfil.json +1 -1
- package/data/playbooks/mcp.json +4 -4
- package/data/playbooks/post-quantum-migration.json +10 -10
- package/data/playbooks/ransomware.json +3 -3
- package/data/playbooks/runtime.json +3 -3
- package/data/playbooks/sbom.json +3 -3
- package/data/playbooks/secrets.json +7 -7
- package/data/playbooks/supply-chain-recovery.json +1 -2
- package/data/playbooks/webhook-callback-abuse.json +1 -1
- package/data/zeroday-lessons.json +804 -399
- package/lib/ttp-mapper.js +1 -1
- package/manifest-snapshot.json +23 -17
- package/manifest-snapshot.sha256 +1 -1
- package/manifest.json +126 -120
- package/package.json +2 -2
- package/sbom.cdx.json +184 -169
- package/scripts/backfill-theater-test.js +10 -10
- package/scripts/builders/did-ladders.js +1 -1
- package/scripts/builders/theater-fingerprints.js +1 -1
- package/scripts/check-atlas-catalog-currency.js +47 -4
- package/scripts/check-ism-control-references.js +221 -0
- package/scripts/check-test-count.js +9 -3
- package/scripts/predeploy.js +9 -0
- package/scripts/refresh-upstream-catalogs.js +9 -2
- package/scripts/release.js +6 -6
- package/scripts/sync-manifest-metadata.js +6 -6
- package/skills/ai-attack-surface/skill.md +11 -9
- package/skills/ai-c2-detection/skill.md +5 -6
- package/skills/ai-risk-management/skill.md +6 -5
- package/skills/api-security/skill.md +8 -8
- package/skills/attack-surface-pentest/skill.md +1 -1
- package/skills/cloud-iam-incident/skill.md +2 -2
- package/skills/cloud-security/skill.md +2 -1
- package/skills/compliance-theater/skill.md +4 -4
- package/skills/coordinated-vuln-disclosure/skill.md +1 -1
- package/skills/decompression-dos/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +3 -3
- package/skills/exploit-scoring/skill.md +3 -3
- package/skills/framework-gap-analysis/skill.md +3 -3
- package/skills/global-grc/skill.md +11 -11
- package/skills/idp-incident-response/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +18 -17
- package/skills/kernel-lpe-triage/skill.md +1 -1
- package/skills/log-injection-telemetry/skill.md +1 -1
- package/skills/mcp-agent-trust/skill.md +5 -4
- package/skills/mlops-security/skill.md +5 -5
- package/skills/multitenancy-isolation/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +1 -1
- package/skills/pqc-first/skill.md +1 -1
- package/skills/privacy-consent-ops/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +1 -1
- package/skills/ransomware-response/skill.md +2 -2
- package/skills/researcher/skill.md +3 -3
- package/skills/sector-financial/skill.md +4 -4
- package/skills/sector-healthcare/skill.md +3 -2
- package/skills/sector-telecom/skill.md +8 -8
- package/skills/security-maturity-tiers/skill.md +8 -8
- package/skills/self-update-integrity/skill.md +1 -1
- package/skills/skill-update-loop/skill.md +3 -3
- package/skills/threat-model-currency/skill.md +6 -6
- package/skills/threat-modeling-methodology/skill.md +4 -4
- 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-
|
|
602
|
-
claim: "
|
|
603
|
-
test: "
|
|
604
|
-
evidence_required: ["
|
|
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-
|
|
640
|
-
claim: "Our
|
|
641
|
-
test: "Confirm
|
|
642
|
-
evidence_required: ["
|
|
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-
|
|
670
|
-
claim: "Our cloud service-account
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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`
|
|
10
|
-
*
|
|
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 (
|
|
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
|
}
|
package/scripts/predeploy.js
CHANGED
|
@@ -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",
|
|
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
|
-
"
|
|
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
|
};
|
package/scripts/release.js
CHANGED
|
@@ -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
|
|
567
|
-
// up to
|
|
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 <
|
|
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 <
|
|
572
|
+
if (_n < 59) {
|
|
573
573
|
console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") +
|
|
574
|
-
", waiting for " + next + " (" + (_n + 1) + "/
|
|
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
|
|
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
|
|
10
|
-
* by replace. The cross-reference arrays are an enriched
|
|
11
|
-
* manifest carries curated refs frontmatter does not — so they
|
|
12
|
-
* replacing them drops the curated refs build-indexes and
|
|
13
|
-
* read.
|
|
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.
|
|
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
|
|
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.
|
|
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-
|
|
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 |
|
|
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.
|
|
184
|
-
| AML.
|
|
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 (
|
|
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.
|
|
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
|
|