@blamejs/exceptd-skills 0.12.13 → 0.12.15
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 +150 -0
- package/bin/exceptd.js +147 -9
- package/data/_indexes/_meta.json +45 -45
- package/data/_indexes/activity-feed.json +4 -4
- package/data/_indexes/catalog-summaries.json +29 -29
- package/data/_indexes/chains.json +3238 -3210
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/jurisdiction-map.json +5 -3
- package/data/_indexes/section-offsets.json +712 -685
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +355 -340
- package/data/atlas-ttps.json +144 -129
- package/data/attack-techniques.json +319 -76
- package/data/cve-catalog.json +515 -475
- package/data/cwe-catalog.json +1081 -759
- package/data/exploit-availability.json +63 -15
- package/data/framework-control-gaps.json +867 -843
- package/data/rfc-references.json +276 -276
- package/keys/EXPECTED_FINGERPRINT +1 -0
- package/lib/auto-discovery.js +21 -4
- package/lib/cross-ref-api.js +39 -6
- package/lib/cve-curation.js +18 -5
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +742 -78
- package/lib/refresh-external.js +40 -22
- package/lib/refresh-network.js +193 -17
- package/lib/scoring.js +20 -7
- package/lib/source-ghsa.js +219 -37
- package/lib/source-osv.js +381 -122
- package/lib/validate-catalog-meta.js +64 -9
- package/lib/validate-cve-catalog.js +56 -18
- package/lib/validate-indexes.js +88 -37
- package/lib/verify.js +72 -0
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -0
- package/manifest.json +73 -73
- package/orchestrator/dispatcher.js +21 -1
- package/orchestrator/event-bus.js +52 -8
- package/orchestrator/index.js +279 -20
- package/orchestrator/pipeline.js +63 -2
- package/orchestrator/scanner.js +32 -10
- package/orchestrator/scheduler.js +150 -17
- package/package.json +3 -1
- package/sbom.cdx.json +7 -7
- package/scripts/check-manifest-snapshot.js +32 -0
- package/scripts/check-sbom-currency.js +65 -3
- package/scripts/check-test-coverage.js +142 -19
- package/scripts/predeploy.js +83 -39
- package/scripts/refresh-manifest-snapshot.js +55 -4
- package/scripts/validate-vendor-online.js +169 -0
- package/scripts/verify-shipped-tarball.js +106 -3
- package/skills/ai-attack-surface/skill.md +18 -10
- package/skills/ai-c2-detection/skill.md +7 -2
- package/skills/ai-risk-management/skill.md +5 -4
- package/skills/api-security/skill.md +3 -3
- package/skills/attack-surface-pentest/skill.md +5 -5
- package/skills/cloud-security/skill.md +1 -1
- package/skills/compliance-theater/skill.md +8 -8
- package/skills/container-runtime-security/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +5 -1
- package/skills/email-security-anti-phishing/skill.md +1 -1
- package/skills/exploit-scoring/skill.md +18 -18
- package/skills/framework-gap-analysis/skill.md +6 -6
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +21 -2
- package/skills/mcp-agent-trust/skill.md +17 -10
- package/skills/mlops-security/skill.md +2 -1
- package/skills/ot-ics-security/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +3 -3
- package/skills/pqc-first/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +7 -3
- package/skills/researcher/skill.md +20 -3
- package/skills/sector-energy/skill.md +1 -1
- package/skills/sector-federal-government/skill.md +1 -1
- package/skills/sector-financial/skill.md +3 -3
- package/skills/sector-healthcare/skill.md +2 -2
- package/skills/security-maturity-tiers/skill.md +7 -7
- package/skills/skill-update-loop/skill.md +19 -3
- package/skills/supply-chain-integrity/skill.md +1 -1
- package/skills/threat-model-currency/skill.md +11 -11
- package/skills/threat-modeling-methodology/skill.md +3 -3
- package/skills/webapp-security/skill.md +1 -1
- package/skills/zeroday-gap-learn/skill.md +51 -7
- package/vendor/blamejs/_PROVENANCE.json +4 -1
- package/vendor/blamejs/worker-pool.js +38 -0
package/scripts/predeploy.js
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* Single-source-of-truth: the GATES list below mirrors the job sequence
|
|
20
20
|
* in .github/workflows/ci.yml. Test coverage in tests/predeploy.test.js
|
|
21
21
|
* asserts the two stay in sync.
|
|
22
|
+
*
|
|
23
|
+
* Audit G F5 — when the manifest-snapshot gate fails, the fix is NOT to
|
|
24
|
+
* run `npm run refresh-snapshot` blindly. The refresh script now refuses
|
|
25
|
+
* unless the operator passes `--commit-only` or sets
|
|
26
|
+
* EXCEPTD_SNAPSHOT_AUDIT_ACK=1. This is intentional: a failing snapshot
|
|
27
|
+
* gate means a breaking change was detected, and an accidental refresh
|
|
28
|
+
* would silently rewrite the baseline. Read the breaking-change list
|
|
29
|
+
* first, then run `node scripts/refresh-manifest-snapshot.js --commit-only`
|
|
30
|
+
* if the change is intentional.
|
|
22
31
|
*/
|
|
23
32
|
|
|
24
33
|
const { execFileSync } = require("child_process");
|
|
@@ -62,28 +71,15 @@ const GATES = [
|
|
|
62
71
|
args: [path.join(ROOT, "lib", "validate-cve-catalog.js")],
|
|
63
72
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
64
73
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "Validate offline RFC catalog state",
|
|
78
|
-
command: process.execPath,
|
|
79
|
-
args: [
|
|
80
|
-
path.join(ROOT, "orchestrator", "index.js"),
|
|
81
|
-
"validate-rfcs",
|
|
82
|
-
"--offline",
|
|
83
|
-
"--no-fail",
|
|
84
|
-
],
|
|
85
|
-
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
86
|
-
},
|
|
74
|
+
// Audit G F13 — the "validate-cves --offline --no-fail" and
|
|
75
|
+
// "validate-rfcs --offline --no-fail" gates were enumeration-only sanity
|
|
76
|
+
// checks: `--no-fail` forced them to always exit 0, so they never blocked
|
|
77
|
+
// a release on a real catalog problem. The deep catalog validation is
|
|
78
|
+
// already performed by the gate above (`lib/validate-cve-catalog.js`),
|
|
79
|
+
// including cross-catalog reference resolution after this same audit.
|
|
80
|
+
// Keeping the no-op gates as predeploy steps inflated the gate count for
|
|
81
|
+
// no marginal value and risked false confidence ("X gates passed"). They
|
|
82
|
+
// are removed in v0.12.14; document the removal in CHANGELOG.
|
|
87
83
|
{
|
|
88
84
|
name: "Manifest snapshot gate (breaking-change detector)",
|
|
89
85
|
command: process.execPath,
|
|
@@ -97,9 +93,13 @@ const GATES = [
|
|
|
97
93
|
ciJobName: "Lint skill files",
|
|
98
94
|
},
|
|
99
95
|
{
|
|
100
|
-
// Informational
|
|
101
|
-
//
|
|
102
|
-
// a
|
|
96
|
+
// Informational — surfaces the forward_watch horizon across all skills.
|
|
97
|
+
// Audit G F12: an exit code of 0 means "ok", 1 means "items present
|
|
98
|
+
// (informational)", 2+ means a runtime error in the gate itself.
|
|
99
|
+
// The runner now distinguishes the two: 0/1 stay informational, 2+
|
|
100
|
+
// surface as a real failure. Pre-fix, any non-zero exit was rolled up
|
|
101
|
+
// as informational, which hid crashes (a 137 OOM looked the same as
|
|
102
|
+
// "found 12 items to review").
|
|
103
103
|
name: "Forward-watch aggregator (informational)",
|
|
104
104
|
command: process.execPath,
|
|
105
105
|
args: [
|
|
@@ -108,6 +108,7 @@ const GATES = [
|
|
|
108
108
|
],
|
|
109
109
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
110
110
|
informational: true,
|
|
111
|
+
informationalMaxExitCode: 1,
|
|
111
112
|
},
|
|
112
113
|
{
|
|
113
114
|
name: "Validate catalog _meta (tlp + source_confidence + freshness_policy)",
|
|
@@ -192,25 +193,60 @@ function runGate(gate) {
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
const t0 = Date.now();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
196
|
+
// Audit G F21 — spawn the child with piped stdio + tee to the parent so we
|
|
197
|
+
// can count `WARN ` lines for the summary table. We still want the live
|
|
198
|
+
// output, so each chunk is forwarded as it arrives.
|
|
199
|
+
const { spawnSync } = require("child_process");
|
|
200
|
+
const r = spawnSync(gate.command, gate.args, {
|
|
201
|
+
cwd: ROOT,
|
|
202
|
+
encoding: "utf8",
|
|
203
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
204
|
+
});
|
|
205
|
+
const durationMs = Date.now() - t0;
|
|
206
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
207
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
208
|
+
// Count WARN-labelled lines in the combined stream so the summary table
|
|
209
|
+
// can surface them. Lint / validate output uses "WARN " at line start;
|
|
210
|
+
// count both the table form and an inline "[warn]" form.
|
|
211
|
+
const combined = (r.stdout || "") + (r.stderr || "");
|
|
212
|
+
const warnCount = (
|
|
213
|
+
combined.match(/^WARN\b/gm) || []
|
|
214
|
+
).length + (
|
|
215
|
+
combined.match(/\[warn\]/g) || []
|
|
216
|
+
).length;
|
|
217
|
+
if (r.status === 0) {
|
|
218
|
+
return { status: "passed", durationMs, warnCount };
|
|
219
|
+
}
|
|
220
|
+
// Audit G F12 — gates may declare informationalMaxExitCode to distinguish
|
|
221
|
+
// "soft signal" (exit codes 0..N) from "crash" (> N). Default behaviour
|
|
222
|
+
// for an informational gate without that field stays the same.
|
|
223
|
+
if (gate.informational) {
|
|
224
|
+
const ceil = typeof gate.informationalMaxExitCode === "number"
|
|
225
|
+
? gate.informationalMaxExitCode
|
|
226
|
+
: Infinity;
|
|
227
|
+
if (r.status !== null && r.status > ceil) {
|
|
200
228
|
return {
|
|
201
|
-
status: "
|
|
202
|
-
exitCode:
|
|
203
|
-
message:
|
|
204
|
-
durationMs
|
|
229
|
+
status: "failed",
|
|
230
|
+
exitCode: r.status,
|
|
231
|
+
message: `informational gate crashed (exit ${r.status} > informationalMaxExitCode=${ceil})`,
|
|
232
|
+
durationMs,
|
|
233
|
+
warnCount,
|
|
205
234
|
};
|
|
206
235
|
}
|
|
207
236
|
return {
|
|
208
|
-
status: "
|
|
209
|
-
exitCode:
|
|
210
|
-
|
|
211
|
-
|
|
237
|
+
status: "informational",
|
|
238
|
+
exitCode: r.status ?? null,
|
|
239
|
+
durationMs,
|
|
240
|
+
warnCount,
|
|
212
241
|
};
|
|
213
242
|
}
|
|
243
|
+
return {
|
|
244
|
+
status: "failed",
|
|
245
|
+
exitCode: r.status ?? null,
|
|
246
|
+
message: r.error ? r.error.message : `exit ${r.status}`,
|
|
247
|
+
durationMs,
|
|
248
|
+
warnCount,
|
|
249
|
+
};
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
function fmtMs(ms) {
|
|
@@ -258,8 +294,16 @@ function main() {
|
|
|
258
294
|
: "✗";
|
|
259
295
|
const timing = fmtMs(outcome.durationMs);
|
|
260
296
|
const timingSuffix = timing ? ` (${timing})` : "";
|
|
297
|
+
// F21 — surface WARN counts so a gate that "passed (3 warnings)" is
|
|
298
|
+
// distinguishable from one that passed cleanly. Pre-fix, warnings
|
|
299
|
+
// printed by individual gates (validate-cve-catalog, lint-skills,
|
|
300
|
+
// validate-playbooks) scrolled past invisible in the summary.
|
|
301
|
+
const warnSuffix =
|
|
302
|
+
outcome.warnCount && outcome.warnCount > 0
|
|
303
|
+
? ` (${outcome.warnCount} warning${outcome.warnCount === 1 ? "" : "s"})`
|
|
304
|
+
: "";
|
|
261
305
|
process.stdout.write(
|
|
262
|
-
` ${icon} ${gate.name.padEnd(widest)} ${outcome.status}${timingSuffix}\n`
|
|
306
|
+
` ${icon} ${gate.name.padEnd(widest)} ${outcome.status}${warnSuffix}${timingSuffix}\n`
|
|
263
307
|
);
|
|
264
308
|
}
|
|
265
309
|
|
|
@@ -11,10 +11,22 @@
|
|
|
11
11
|
* blindly — read the breaking-change list first. A breaking change is
|
|
12
12
|
* a surface narrowing every downstream consumer needs to know about.
|
|
13
13
|
*
|
|
14
|
+
* Audit G F5 — commitOnly mode. Pass `--commit-only` (or set the env
|
|
15
|
+
* EXCEPTD_SNAPSHOT_AUDIT_ACK=1) to acknowledge that the operator
|
|
16
|
+
* deliberately wants to overwrite the committed snapshot. When neither
|
|
17
|
+
* flag nor env is set AND the snapshot would actually change, the
|
|
18
|
+
* script refuses and emits a structured diff hint. This stops an
|
|
19
|
+
* accidental `npm run refresh-snapshot` (run as muscle-memory while
|
|
20
|
+
* triaging a failing gate) from masking a real breaking change.
|
|
21
|
+
*
|
|
14
22
|
* Usage:
|
|
15
|
-
* node scripts/refresh-manifest-snapshot.js
|
|
16
|
-
*
|
|
17
|
-
*
|
|
23
|
+
* node scripts/refresh-manifest-snapshot.js # dry-shows the diff
|
|
24
|
+
* EXCEPTD_SNAPSHOT_AUDIT_ACK=1 \
|
|
25
|
+
* node scripts/refresh-manifest-snapshot.js # writes the new snapshot
|
|
26
|
+
* node scripts/refresh-manifest-snapshot.js --commit-only # same thing, on argv
|
|
27
|
+
*
|
|
28
|
+
* The flag is documented in scripts/predeploy.js so contributors see it
|
|
29
|
+
* the moment the snapshot gate fails.
|
|
18
30
|
*/
|
|
19
31
|
|
|
20
32
|
const fs = require("fs");
|
|
@@ -50,8 +62,47 @@ function captureSurface(manifest) {
|
|
|
50
62
|
|
|
51
63
|
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
52
64
|
const snapshot = captureSurface(manifest);
|
|
65
|
+
const newJson = JSON.stringify(snapshot, null, 2) + "\n";
|
|
66
|
+
|
|
67
|
+
// F5 — refuse to overwrite an existing snapshot unless the operator
|
|
68
|
+
// has explicitly acknowledged the rewrite (env or --commit-only flag).
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
const commitOnly =
|
|
71
|
+
argv.includes("--commit-only") ||
|
|
72
|
+
process.env.EXCEPTD_SNAPSHOT_AUDIT_ACK === "1";
|
|
53
73
|
|
|
54
|
-
fs.
|
|
74
|
+
if (fs.existsSync(SNAPSHOT_PATH) && !commitOnly) {
|
|
75
|
+
const current = fs.readFileSync(SNAPSHOT_PATH, "utf8");
|
|
76
|
+
// Normalise the _generated_at timestamp for comparison — that field
|
|
77
|
+
// changes every run and shouldn't trigger the guard.
|
|
78
|
+
const stripGenerated = (s) => s.replace(
|
|
79
|
+
/"_generated_at":\s*"[^"]+",?\s*\n?/, ""
|
|
80
|
+
);
|
|
81
|
+
if (stripGenerated(current) === stripGenerated(newJson)) {
|
|
82
|
+
console.log("[refresh-manifest-snapshot] snapshot unchanged — nothing to do.");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
"[refresh-manifest-snapshot] REFUSING to overwrite manifest-snapshot.json — " +
|
|
87
|
+
"the captured surface differs from the committed snapshot.\n" +
|
|
88
|
+
" Re-run with `--commit-only` (or EXCEPTD_SNAPSHOT_AUDIT_ACK=1) to confirm " +
|
|
89
|
+
"the rewrite is intentional. The check-manifest-snapshot.js gate exists to " +
|
|
90
|
+
"force a deliberate decision about removed skills / triggers / refs before " +
|
|
91
|
+
"the baseline is rewritten.\n"
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(SNAPSHOT_PATH, newJson, "utf8");
|
|
55
97
|
|
|
56
98
|
console.log(`[refresh-manifest-snapshot] wrote ${snapshot.skill_count} skills to manifest-snapshot.json`);
|
|
57
99
|
console.log("[refresh-manifest-snapshot] commit this file alongside the surface change.");
|
|
100
|
+
|
|
101
|
+
// Audit G F23 — write a tracked SHA-256 of the snapshot so the
|
|
102
|
+
// check-manifest-snapshot.js gate can verify integrity (no hand edits
|
|
103
|
+
// after refresh).
|
|
104
|
+
const crypto = require("crypto");
|
|
105
|
+
const snapshotSha = crypto.createHash("sha256").update(newJson).digest("hex");
|
|
106
|
+
const snapshotShaPath = path.join(ROOT, "manifest-snapshot.sha256");
|
|
107
|
+
fs.writeFileSync(snapshotShaPath, snapshotSha + " manifest-snapshot.json\n", "utf8");
|
|
108
|
+
console.log(`[refresh-manifest-snapshot] wrote integrity hash to manifest-snapshot.sha256`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* scripts/validate-vendor-online.js — Audit G F6.
|
|
5
|
+
*
|
|
6
|
+
* Optional, network-touching companion to lib/validate-vendor.js. For every
|
|
7
|
+
* file recorded in vendor/blamejs/_PROVENANCE.json, fetches the upstream
|
|
8
|
+
* blob from github.com/<source_repo>/blob/<pinned_commit>/<upstream_path>
|
|
9
|
+
* (via the raw.githubusercontent.com mirror), hashes it, and compares the
|
|
10
|
+
* result against the `upstream_sha256_at_pin` recorded in _PROVENANCE.json.
|
|
11
|
+
*
|
|
12
|
+
* This catches the class where _PROVENANCE.json was hand-edited to
|
|
13
|
+
* advertise a `upstream_sha256_at_pin` that does not actually match what
|
|
14
|
+
* upstream had at that commit. lib/validate-vendor.js only checks that the
|
|
15
|
+
* local vendored file matches its own recorded hash — that's self-attesting.
|
|
16
|
+
* This script extends the check to upstream, closing the gap.
|
|
17
|
+
*
|
|
18
|
+
* Not part of `npm run predeploy` by default — the predeploy gate sequence
|
|
19
|
+
* must remain network-independent (offline gates only). Run manually:
|
|
20
|
+
*
|
|
21
|
+
* node scripts/validate-vendor-online.js
|
|
22
|
+
* node scripts/validate-vendor-online.js --timeout 30000
|
|
23
|
+
* node scripts/validate-vendor-online.js --json
|
|
24
|
+
*
|
|
25
|
+
* Exit codes:
|
|
26
|
+
* 0 every vendored file's upstream_sha256_at_pin matched upstream
|
|
27
|
+
* 1 at least one mismatch
|
|
28
|
+
* 2 runtime / network error
|
|
29
|
+
*
|
|
30
|
+
* Zero npm deps. Node 24 stdlib only.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require("fs");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
const crypto = require("crypto");
|
|
36
|
+
const https = require("https");
|
|
37
|
+
|
|
38
|
+
const ROOT = path.join(__dirname, "..");
|
|
39
|
+
const PROV_PATH = path.join(ROOT, "vendor", "blamejs", "_PROVENANCE.json");
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const out = { timeoutMs: 15000, json: false };
|
|
43
|
+
for (let i = 2; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === "--timeout") out.timeoutMs = Number(argv[++i]) || out.timeoutMs;
|
|
46
|
+
else if (a === "--json") out.json = true;
|
|
47
|
+
else if (a === "--help" || a === "-h") {
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
"Usage: node scripts/validate-vendor-online.js [--timeout <ms>] [--json]\n"
|
|
50
|
+
);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} else {
|
|
53
|
+
process.stderr.write(`Unknown argument: ${a}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rawUrlForPin(sourceRepo, commit, upstreamPath) {
|
|
61
|
+
// Translate https://github.com/owner/repo → raw.githubusercontent.com/owner/repo
|
|
62
|
+
// sourceRepo may end in .git; strip it. Tolerate trailing slash.
|
|
63
|
+
const m = (sourceRepo || "").match(
|
|
64
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/
|
|
65
|
+
);
|
|
66
|
+
if (!m) return null;
|
|
67
|
+
const [, owner, repo] = m;
|
|
68
|
+
const cleanPath = String(upstreamPath || "").replace(/^\/+/, "");
|
|
69
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${commit}/${cleanPath}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MAX_REDIRECTS = 5;
|
|
73
|
+
|
|
74
|
+
function fetchBuffer(url, timeoutMs, redirectsLeft = MAX_REDIRECTS) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const req = https.get(url, (res) => {
|
|
77
|
+
// v0.12.14 (codex P2): cap redirect hops. A redirect loop (or a
|
|
78
|
+
// hostile / mis-configured upstream that keeps returning 3xx with
|
|
79
|
+
// Location pointing back to itself) used to recurse until stack
|
|
80
|
+
// overflow or hang. Now: count hops, fail clean on exhaustion.
|
|
81
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
82
|
+
res.resume();
|
|
83
|
+
if (redirectsLeft <= 0) {
|
|
84
|
+
return reject(new Error(`exceeded ${MAX_REDIRECTS} redirects fetching ${url}`));
|
|
85
|
+
}
|
|
86
|
+
return resolve(fetchBuffer(res.headers.location, timeoutMs, redirectsLeft - 1));
|
|
87
|
+
}
|
|
88
|
+
if (res.statusCode !== 200) {
|
|
89
|
+
res.resume();
|
|
90
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
91
|
+
}
|
|
92
|
+
const chunks = [];
|
|
93
|
+
res.on("data", (c) => chunks.push(c));
|
|
94
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
95
|
+
res.on("error", reject);
|
|
96
|
+
});
|
|
97
|
+
req.on("error", reject);
|
|
98
|
+
req.setTimeout(timeoutMs, () => {
|
|
99
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms fetching ${url}`));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
const opts = parseArgs(process.argv);
|
|
106
|
+
if (!fs.existsSync(PROV_PATH)) {
|
|
107
|
+
process.stderr.write(`vendor/blamejs/_PROVENANCE.json missing\n`);
|
|
108
|
+
process.exitCode = 2;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const prov = JSON.parse(fs.readFileSync(PROV_PATH, "utf8"));
|
|
112
|
+
const sourceRepo = prov.source_repo;
|
|
113
|
+
const pinnedCommit = prov.pinned_commit;
|
|
114
|
+
if (!sourceRepo || !pinnedCommit) {
|
|
115
|
+
process.stderr.write(`_PROVENANCE.json missing source_repo or pinned_commit\n`);
|
|
116
|
+
process.exitCode = 2;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const findings = [];
|
|
121
|
+
for (const [name, info] of Object.entries(prov.files || {})) {
|
|
122
|
+
const url = rawUrlForPin(sourceRepo, pinnedCommit, info.upstream_path);
|
|
123
|
+
if (!url) {
|
|
124
|
+
findings.push({ name, ok: false, reason: `cannot compute raw URL for ${sourceRepo}` });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const buf = await fetchBuffer(url, opts.timeoutMs);
|
|
129
|
+
const sha = crypto.createHash("sha256").update(buf).digest("hex");
|
|
130
|
+
if (info.upstream_sha256_at_pin && sha !== info.upstream_sha256_at_pin) {
|
|
131
|
+
findings.push({
|
|
132
|
+
name,
|
|
133
|
+
ok: false,
|
|
134
|
+
reason:
|
|
135
|
+
`upstream sha mismatch: recorded ${String(info.upstream_sha256_at_pin).slice(0, 12)}…, ` +
|
|
136
|
+
`live ${sha.slice(0, 12)}…`,
|
|
137
|
+
url,
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
findings.push({ name, ok: true, sha, url });
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
findings.push({ name, ok: false, reason: `fetch failed: ${e.message}`, url });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const failed = findings.filter((f) => !f.ok);
|
|
148
|
+
if (opts.json) {
|
|
149
|
+
process.stdout.write(JSON.stringify({ ok: failed.length === 0, findings }, null, 2) + "\n");
|
|
150
|
+
} else {
|
|
151
|
+
for (const f of findings) {
|
|
152
|
+
if (f.ok) process.stdout.write(`PASS ${f.name} ${f.sha.slice(0, 12)}…\n`);
|
|
153
|
+
else process.stdout.write(`FAIL ${f.name} ${f.reason}\n`);
|
|
154
|
+
}
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
`\n${findings.length - failed.length}/${findings.length} vendored files match upstream pin.\n`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
process.exitCode = failed.length === 0 ? 0 : 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
main().catch((e) => {
|
|
164
|
+
process.stderr.write(`runtime error: ${e.message}\n`);
|
|
165
|
+
process.exitCode = 2;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { rawUrlForPin, fetchBuffer };
|
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
* The bug was invisible because CI's verify ran against the SOURCE tree,
|
|
19
19
|
* not the shipped tarball. This gate closes that gap.
|
|
20
20
|
*
|
|
21
|
+
* Audit G:
|
|
22
|
+
* F9 — After the first-pass extraction (using the source-tree parseTar),
|
|
23
|
+
* re-parse the tarball using the parseTar shipped INSIDE the
|
|
24
|
+
* extracted tree itself. If the two parses disagree, fail with a
|
|
25
|
+
* structured error. Catches the class where the shipped parser
|
|
26
|
+
* silently rejects entries the source parser accepts (or vice
|
|
27
|
+
* versa), which would mean operators run a different extractor
|
|
28
|
+
* than CI exercised.
|
|
29
|
+
* F15 — Invoke `npm pack --offline` so the gate cannot be blocked by
|
|
30
|
+
* registry reachability problems during predeploy.
|
|
31
|
+
* F4 — Cross-check the extracted public.pem against
|
|
32
|
+
* keys/EXPECTED_FINGERPRINT (warn-and-continue when missing, fail
|
|
33
|
+
* when present-but-mismatched and KEYS_ROTATED != 1).
|
|
34
|
+
*
|
|
21
35
|
* Exit codes:
|
|
22
36
|
* 0 verify passed against the packed tarball
|
|
23
37
|
* 1 verify failed against the packed tarball (the bug class above)
|
|
@@ -42,7 +56,10 @@ function fail(msg, code = 1) {
|
|
|
42
56
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "verify-shipped-"));
|
|
43
57
|
try {
|
|
44
58
|
emit(`packing into ${tmpRoot} ...`);
|
|
45
|
-
|
|
59
|
+
// F15 — pass --offline. Predeploy must run without registry
|
|
60
|
+
// reachability; `npm pack` does not need the network for a local
|
|
61
|
+
// package and forcing offline mode hard-locks the assumption.
|
|
62
|
+
const pack = spawnSync("npm", ["pack", "--offline", "--pack-destination", tmpRoot], {
|
|
46
63
|
cwd: ROOT,
|
|
47
64
|
encoding: "utf8",
|
|
48
65
|
shell: process.platform === "win32",
|
|
@@ -60,10 +77,10 @@ try {
|
|
|
60
77
|
const extractDir = path.join(tmpRoot, "extract");
|
|
61
78
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
62
79
|
const zlib = require("zlib");
|
|
63
|
-
const { parseTar } = require(path.join(ROOT, "lib", "refresh-network.js"));
|
|
80
|
+
const { parseTar: parseTarSource } = require(path.join(ROOT, "lib", "refresh-network.js"));
|
|
64
81
|
const tgz = fs.readFileSync(tarballPath);
|
|
65
82
|
const tarBuf = zlib.gunzipSync(tgz);
|
|
66
|
-
const entries =
|
|
83
|
+
const entries = parseTarSource(tarBuf);
|
|
67
84
|
for (const e of entries) {
|
|
68
85
|
if (!e.name) continue;
|
|
69
86
|
const dst = path.join(extractDir, e.name);
|
|
@@ -77,6 +94,65 @@ try {
|
|
|
77
94
|
}
|
|
78
95
|
emit(`extracted to ${pkgRoot}`);
|
|
79
96
|
|
|
97
|
+
// Audit G F9 — load the extracted tree's OWN parseTar and re-parse the
|
|
98
|
+
// tarball. If the two parsers diverge on entry list or content, the
|
|
99
|
+
// gate trips: this means CI exercised a different parser than operators
|
|
100
|
+
// will. Defense against drift between source and shipped tarball when
|
|
101
|
+
// someone edits lib/refresh-network.js without re-vendoring or vice
|
|
102
|
+
// versa.
|
|
103
|
+
const shippedParserPath = path.join(pkgRoot, "lib", "refresh-network.js");
|
|
104
|
+
if (!fs.existsSync(shippedParserPath)) {
|
|
105
|
+
fail(`extracted tree missing lib/refresh-network.js (cannot run F9 cross-parse check)`, 2);
|
|
106
|
+
}
|
|
107
|
+
let parseTarShipped;
|
|
108
|
+
try {
|
|
109
|
+
parseTarShipped = require(shippedParserPath).parseTar;
|
|
110
|
+
} catch (e) {
|
|
111
|
+
fail(`failed to load extracted parseTar: ${e.message}`, 2);
|
|
112
|
+
}
|
|
113
|
+
if (typeof parseTarShipped !== "function") {
|
|
114
|
+
fail(`extracted lib/refresh-network.js does not export parseTar`, 2);
|
|
115
|
+
}
|
|
116
|
+
const shippedEntries = parseTarShipped(tarBuf);
|
|
117
|
+
// Compare counts first — fast bailout.
|
|
118
|
+
const divergences = [];
|
|
119
|
+
if (shippedEntries.length !== entries.length) {
|
|
120
|
+
divergences.push(
|
|
121
|
+
`entry count divergence: source-tree parser produced ${entries.length}, ` +
|
|
122
|
+
`shipped parser produced ${shippedEntries.length}`
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
// Walk in parallel; tarball entry order is deterministic so positional
|
|
126
|
+
// compare is correct. Compare name + byte length + body bytes.
|
|
127
|
+
for (let i = 0; i < entries.length; i++) {
|
|
128
|
+
const a = entries[i];
|
|
129
|
+
const b = shippedEntries[i];
|
|
130
|
+
if (a.name !== b.name) {
|
|
131
|
+
divergences.push(`entry[${i}] name mismatch: source=${a.name} shipped=${b.name}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const aBuf = Buffer.isBuffer(a.body) ? a.body : Buffer.from(a.body);
|
|
135
|
+
const bBuf = Buffer.isBuffer(b.body) ? b.body : Buffer.from(b.body);
|
|
136
|
+
if (aBuf.length !== bBuf.length || !aBuf.equals(bBuf)) {
|
|
137
|
+
divergences.push(
|
|
138
|
+
`entry[${i}] (${a.name}) body bytes differ between source-tree and shipped parser ` +
|
|
139
|
+
`(source ${aBuf.length} bytes vs shipped ${bBuf.length} bytes)`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (divergences.length > 0) {
|
|
145
|
+
emit(`*** F9: parseTar divergence between source-tree and shipped tree ***`);
|
|
146
|
+
for (const d of divergences.slice(0, 5)) emit(` - ${d}`);
|
|
147
|
+
if (divergences.length > 5) emit(` ... and ${divergences.length - 5} more`);
|
|
148
|
+
fail(
|
|
149
|
+
`parseTar implementations diverge between source tree and shipped tarball. ` +
|
|
150
|
+
`Operators will run a different extractor than CI exercised. Refusing to publish.`,
|
|
151
|
+
1
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
emit(`F9: source-tree and shipped parseTar agree on ${entries.length} entries`);
|
|
155
|
+
|
|
80
156
|
// Run the verifier inline against the extracted package tree. This avoids
|
|
81
157
|
// having to spawn a separate process whose cwd resolution differs across
|
|
82
158
|
// platforms.
|
|
@@ -108,6 +184,33 @@ try {
|
|
|
108
184
|
emit(`*** Something between sign and pack is swapping the key. Verify will fail below. ***`);
|
|
109
185
|
}
|
|
110
186
|
|
|
187
|
+
// Audit G F4 — key-pin cross-check against the EXTRACTED tree. The pin
|
|
188
|
+
// is consumed from keys/EXPECTED_FINGERPRINT in the extracted package —
|
|
189
|
+
// that's the file operators will actually receive on `npm install`.
|
|
190
|
+
// Warn when absent, fail when present-but-mismatched (unless KEYS_ROTATED).
|
|
191
|
+
const expectedFpPath = path.join(pkgRoot, "keys", "EXPECTED_FINGERPRINT");
|
|
192
|
+
if (fs.existsSync(expectedFpPath)) {
|
|
193
|
+
const raw = fs.readFileSync(expectedFpPath, "utf8").trim();
|
|
194
|
+
const firstLine = raw.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) || "";
|
|
195
|
+
const liveFpLine = `SHA256:${pubFp}`;
|
|
196
|
+
if (firstLine !== liveFpLine) {
|
|
197
|
+
if (process.env.KEYS_ROTATED === "1") {
|
|
198
|
+
emit(`WARN: extracted public.pem fingerprint ${liveFpLine} differs from pin ${firstLine}; KEYS_ROTATED=1 accepted`);
|
|
199
|
+
} else {
|
|
200
|
+
fail(
|
|
201
|
+
`keys/EXPECTED_FINGERPRINT (${firstLine}) does not match the extracted ` +
|
|
202
|
+
`public.pem fingerprint (${liveFpLine}). If this is an intentional rotation ` +
|
|
203
|
+
`set KEYS_ROTATED=1 and commit the new pin.`,
|
|
204
|
+
1
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
emit(`F4: key pin verified — ${liveFpLine} matches keys/EXPECTED_FINGERPRINT`);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
emit(`WARN: keys/EXPECTED_FINGERPRINT not in extracted tree — key-pin check skipped`);
|
|
212
|
+
}
|
|
213
|
+
|
|
111
214
|
let pass = 0, miss = 0, fail_count = 0;
|
|
112
215
|
const failures = [];
|
|
113
216
|
for (const s of (manifest.skills || [])) {
|
|
@@ -63,7 +63,7 @@ The AI attack surface is not speculative. It is actively exploited. The followin
|
|
|
63
63
|
|
|
64
64
|
### 1. Prompt Injection as Enterprise RCE
|
|
65
65
|
|
|
66
|
-
**CVE-2025-53773** — Hidden prompt injection in GitHub Copilot
|
|
66
|
+
**CVE-2025-53773** — Hidden prompt injection in GitHub Copilot agent mode coerces the assistant to write `"chat.tools.autoApprove": true` into `.vscode/settings.json`, flipping every subsequent tool call into auto-approval. CVSS 7.8 (AV:L — local-vector through developer-side IDE interaction; RWEP 30). The attack embeds adversarial instructions in any agent-readable content (source comments, README, PR descriptions, retrieved docs, MCP tool responses). Once the YOLO-mode flag lands, the next shell tool call executes attacker-chosen commands in the developer's user context.
|
|
67
67
|
|
|
68
68
|
This is not a chatbot trick. This is enterprise RCE via a developer tool used by hundreds of millions of developers. The attack surface is any system that:
|
|
69
69
|
- Feeds external content (user input, web content, documents, PR descriptions, emails, calendar events) into an LLM prompt
|
|
@@ -71,13 +71,13 @@ This is not a chatbot trick. This is enterprise RCE via a developer tool used by
|
|
|
71
71
|
|
|
72
72
|
**Attack success rates against SOTA defenses:** A 2026 meta-analysis of 78 studies found adaptive prompt injection strategies succeed against state-of-the-art defenses at rates exceeding 85%. No current framework has adequate controls for this.
|
|
73
73
|
|
|
74
|
-
**ATLAS ref:** AML.T0054 (
|
|
74
|
+
**ATLAS ref:** AML.T0054 (LLM Jailbreak) and AML.T0051 (LLM Prompt Injection)
|
|
75
75
|
|
|
76
76
|
### 2. MCP Supply Chain — Architectural RCE
|
|
77
77
|
|
|
78
78
|
The Model Context Protocol (MCP) introduced an architectural vulnerability affecting every major AI coding assistant: Cursor, VS Code + GitHub Copilot, Windsurf, Claude Code, Gemini CLI.
|
|
79
79
|
|
|
80
|
-
**CVE-2026-30615** — Windsurf.
|
|
80
|
+
**CVE-2026-30615** — Windsurf MCP. CVSS 8.0 (AV:L — local-vector RCE requiring attacker-controlled HTML the MCP client processes; RWEP 35). The vulnerability allows a malicious or compromised MCP server to drive code execution in the context of the AI assistant once a victim installs it. 150M+ combined downloads across MCP-capable assistants share the same architectural attack surface.
|
|
81
81
|
|
|
82
82
|
This is a supply chain attack surface. Every MCP server a user installs is a potential RCE vector. Trust boundaries that exist for npm packages do not exist for MCP servers because most MCP clients do not enforce signed manifests or tool allowlists.
|
|
83
83
|
|
|
@@ -89,13 +89,13 @@ This is a supply chain attack surface. Every MCP server a user installs is a pot
|
|
|
89
89
|
|
|
90
90
|
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.
|
|
91
91
|
|
|
92
|
-
**ATLAS ref:** AML.
|
|
92
|
+
**ATLAS ref:** AML.T0016 (Obtain Capabilities: Develop Capabilities)
|
|
93
93
|
|
|
94
94
|
### 4. AI Credential Phishing Acceleration
|
|
95
95
|
|
|
96
96
|
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.
|
|
97
97
|
|
|
98
|
-
**ATLAS ref:** AML.
|
|
98
|
+
**ATLAS ref:** AML.T0016 (Obtain Capabilities: Develop Capabilities — misuse of public AI APIs to generate phishing payloads)
|
|
99
99
|
|
|
100
100
|
### 5. AI as Covert C2 — SesameOp
|
|
101
101
|
|
|
@@ -127,6 +127,14 @@ Training pipeline targeting has moved beyond data injection to directly biasing
|
|
|
127
127
|
|
|
128
128
|
AI-assisted reconnaissance is observed at 36,000 probes per second per campaign. Traditional rate-based detection (100–1,000 req/s threshold alerts) does not fire at legitimate-looking distributed AI-directed probe rates until significant reconnaissance has already occurred.
|
|
129
129
|
|
|
130
|
+
### 10. LLM-Gateway Credential Theft as AI Attack Surface
|
|
131
|
+
|
|
132
|
+
**CVE-2026-42208** — BerriAI LiteLLM Proxy authorization-header SQL injection (CVSS 9.8 / CVSS v4 9.3 / CISA KEV-listed 2026-05-08, due 2026-05-29). LiteLLM is the open-source LLM-API gateway used in front of agent stacks, MCP-server fronts, and multi-model proxy deployments — exactly the trust hinge that this skill's threat-context section treats as the credential boundary for hosted-model use. The proxy concatenated an attacker-controlled `Authorization` header value into a SQL query in the error-logging path, so a single curl-able POST against `/chat/completions` with a SQL-injection payload returns the managed-credentials DB content without prior auth. Patched in 1.83.7+; temporary workaround `general_settings: disable_error_logs: true`. Any organisation whose AI attack-surface inventory treats the LLM gateway as "just a reverse proxy" misses that the gateway holds every downstream model-provider credential.
|
|
133
|
+
|
|
134
|
+
### 11. AI-Discovered + AI-Weaponized Supply-Chain Worms
|
|
135
|
+
|
|
136
|
+
**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 (ML Supply Chain Compromise) threat applied to JS toolchains that the AI assistant ecosystem depends on.
|
|
137
|
+
|
|
130
138
|
---
|
|
131
139
|
|
|
132
140
|
## Framework Lag Declaration
|
|
@@ -150,14 +158,14 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
|
|
|
150
158
|
|
|
151
159
|
| ATLAS ID | Technique | Framework Coverage | Gap Description | Exploitation Example |
|
|
152
160
|
|---|---|---|---|---|
|
|
153
|
-
| AML.T0054 |
|
|
161
|
+
| 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) |
|
|
154
162
|
| AML.T0010 | ML 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) |
|
|
155
163
|
| 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 |
|
|
156
164
|
| AML.T0020 | Poison Training Data | Partial (NIST AI RMF) | NIST AI RMF identifies the risk; no specific technical control | Supply chain logistics model poisoning |
|
|
157
165
|
| AML.T0043 | Craft Adversarial Data | Partial (SI-10) | SI-10 covers web input validation; not semantic injection in LLM prompts | RAG vector manipulation |
|
|
158
166
|
| 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 |
|
|
159
|
-
| AML.T0017 |
|
|
160
|
-
| AML.T0016 |
|
|
167
|
+
| 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 |
|
|
168
|
+
| 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 |
|
|
161
169
|
| AML.T0018 | Backdoor ML Model | Partial (NIST AI RMF) | No technical control requirements for model integrity verification | Training pipeline poisoning |
|
|
162
170
|
|
|
163
171
|
---
|
|
@@ -166,8 +174,8 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
|
|
|
166
174
|
|
|
167
175
|
| Vulnerability | CVSS | RWEP | KEV | PoC | AI-Accelerated | Active Exploitation |
|
|
168
176
|
|---|---|---|---|---|---|---|
|
|
169
|
-
| CVE-2025-53773 (Copilot
|
|
170
|
-
| CVE-2026-30615 (Windsurf MCP RCE) |
|
|
177
|
+
| CVE-2025-53773 (Copilot YOLO-mode RCE) | 7.8 | 30 | No | Yes — demonstrated | Yes (AI tooling enables) | Suspected |
|
|
178
|
+
| CVE-2026-30615 (Windsurf MCP local-vector RCE) | 8.0 | 35 | No | Partial | No | Suspected |
|
|
171
179
|
| SesameOp (AI C2 technique) | N/A | N/A | N/A | Yes (ATLAS documented) | Yes | Confirmed campaign |
|
|
172
180
|
| PROMPTFLUX family | N/A | N/A | N/A | Behavioral signatures | Yes | Active |
|
|
173
181
|
| PROMPTSTEAL family | N/A | N/A | N/A | Behavioral signatures | Yes | Active |
|