@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/bin/exceptd.js +147 -9
  3. package/data/_indexes/_meta.json +45 -45
  4. package/data/_indexes/activity-feed.json +4 -4
  5. package/data/_indexes/catalog-summaries.json +29 -29
  6. package/data/_indexes/chains.json +3238 -3210
  7. package/data/_indexes/frequency.json +3 -0
  8. package/data/_indexes/jurisdiction-map.json +5 -3
  9. package/data/_indexes/section-offsets.json +712 -685
  10. package/data/_indexes/theater-fingerprints.json +1 -1
  11. package/data/_indexes/token-budget.json +355 -340
  12. package/data/atlas-ttps.json +144 -129
  13. package/data/attack-techniques.json +319 -76
  14. package/data/cve-catalog.json +515 -475
  15. package/data/cwe-catalog.json +1081 -759
  16. package/data/exploit-availability.json +63 -15
  17. package/data/framework-control-gaps.json +867 -843
  18. package/data/rfc-references.json +276 -276
  19. package/keys/EXPECTED_FINGERPRINT +1 -0
  20. package/lib/auto-discovery.js +21 -4
  21. package/lib/cross-ref-api.js +39 -6
  22. package/lib/cve-curation.js +18 -5
  23. package/lib/lint-skills.js +6 -1
  24. package/lib/playbook-runner.js +742 -78
  25. package/lib/refresh-external.js +40 -22
  26. package/lib/refresh-network.js +193 -17
  27. package/lib/scoring.js +20 -7
  28. package/lib/source-ghsa.js +219 -37
  29. package/lib/source-osv.js +381 -122
  30. package/lib/validate-catalog-meta.js +64 -9
  31. package/lib/validate-cve-catalog.js +56 -18
  32. package/lib/validate-indexes.js +88 -37
  33. package/lib/verify.js +72 -0
  34. package/manifest-snapshot.json +1 -1
  35. package/manifest-snapshot.sha256 +1 -0
  36. package/manifest.json +73 -73
  37. package/orchestrator/dispatcher.js +21 -1
  38. package/orchestrator/event-bus.js +52 -8
  39. package/orchestrator/index.js +279 -20
  40. package/orchestrator/pipeline.js +63 -2
  41. package/orchestrator/scanner.js +32 -10
  42. package/orchestrator/scheduler.js +150 -17
  43. package/package.json +3 -1
  44. package/sbom.cdx.json +7 -7
  45. package/scripts/check-manifest-snapshot.js +32 -0
  46. package/scripts/check-sbom-currency.js +65 -3
  47. package/scripts/check-test-coverage.js +142 -19
  48. package/scripts/predeploy.js +83 -39
  49. package/scripts/refresh-manifest-snapshot.js +55 -4
  50. package/scripts/validate-vendor-online.js +169 -0
  51. package/scripts/verify-shipped-tarball.js +106 -3
  52. package/skills/ai-attack-surface/skill.md +18 -10
  53. package/skills/ai-c2-detection/skill.md +7 -2
  54. package/skills/ai-risk-management/skill.md +5 -4
  55. package/skills/api-security/skill.md +3 -3
  56. package/skills/attack-surface-pentest/skill.md +5 -5
  57. package/skills/cloud-security/skill.md +1 -1
  58. package/skills/compliance-theater/skill.md +8 -8
  59. package/skills/container-runtime-security/skill.md +1 -1
  60. package/skills/dlp-gap-analysis/skill.md +5 -1
  61. package/skills/email-security-anti-phishing/skill.md +1 -1
  62. package/skills/exploit-scoring/skill.md +18 -18
  63. package/skills/framework-gap-analysis/skill.md +6 -6
  64. package/skills/global-grc/skill.md +3 -2
  65. package/skills/identity-assurance/skill.md +2 -2
  66. package/skills/incident-response-playbook/skill.md +4 -4
  67. package/skills/kernel-lpe-triage/skill.md +21 -2
  68. package/skills/mcp-agent-trust/skill.md +17 -10
  69. package/skills/mlops-security/skill.md +2 -1
  70. package/skills/ot-ics-security/skill.md +1 -1
  71. package/skills/policy-exception-gen/skill.md +3 -3
  72. package/skills/pqc-first/skill.md +1 -1
  73. package/skills/rag-pipeline-security/skill.md +7 -3
  74. package/skills/researcher/skill.md +20 -3
  75. package/skills/sector-energy/skill.md +1 -1
  76. package/skills/sector-federal-government/skill.md +1 -1
  77. package/skills/sector-financial/skill.md +3 -3
  78. package/skills/sector-healthcare/skill.md +2 -2
  79. package/skills/security-maturity-tiers/skill.md +7 -7
  80. package/skills/skill-update-loop/skill.md +19 -3
  81. package/skills/supply-chain-integrity/skill.md +1 -1
  82. package/skills/threat-model-currency/skill.md +11 -11
  83. package/skills/threat-modeling-methodology/skill.md +3 -3
  84. package/skills/webapp-security/skill.md +1 -1
  85. package/skills/zeroday-gap-learn/skill.md +51 -7
  86. package/vendor/blamejs/_PROVENANCE.json +4 -1
  87. package/vendor/blamejs/worker-pool.js +38 -0
@@ -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
- name: "Validate offline CVE catalog state",
67
- command: process.execPath,
68
- args: [
69
- path.join(ROOT, "orchestrator", "index.js"),
70
- "validate-cves",
71
- "--offline",
72
- "--no-fail",
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 only — surfaces the forward_watch horizon across all
101
- // skills as a sanity signal. Emits the count but never fails the run;
102
- // a parse problem is reported, not blocking.
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
- try {
196
- execFileSync(gate.command, gate.args, { stdio: "inherit", cwd: ROOT });
197
- return { status: "passed", durationMs: Date.now() - t0 };
198
- } catch (e) {
199
- if (gate.informational) {
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: "informational",
202
- exitCode: e.status ?? null,
203
- message: e.message,
204
- durationMs: Date.now() - t0,
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: "failed",
209
- exitCode: e.status ?? null,
210
- message: e.message,
211
- durationMs: Date.now() - t0,
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
- * git add manifest-snapshot.json
17
- * git commit -m "refresh manifest snapshot: <what changed>"
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.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
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
- const pack = spawnSync("npm", ["pack", "--pack-destination", tmpRoot], {
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 = parseTar(tarBuf);
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 PR descriptions enabling RCE. CVSS 9.6. The attack embeds adversarial instructions in GitHub PR descriptions. When a developer uses GitHub Copilot to review or summarize the PR, the injected instructions execute in the context of the developer's session, enabling remote code execution.
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 (Craft Adversarial Data NLP)
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. Zero user interaction required. The vulnerability allows a malicious MCP server (or a compromised legitimate MCP server) to execute arbitrary code in the context of the AI assistant. 150M+ affected downloads.
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.T0017 (Develop Capabilities)
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.T0018 (Acquire Public ML Artifacts — misuse of generation capability)
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 | Craft Adversarial Data — NLP | Missing in all major frameworks | No control covers adversarial text injection into LLM prompts | CVE-2025-53773 (GitHub Copilot RCE) |
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 | Develop Capabilities | Partial (awareness only) | No framework requires monitoring for AI-assisted exploit development against the org | Copy Fail AI discovery, 41% of 2025 0-days |
160
- | AML.T0016 | Acquire Public ML Artifacts | Missing (misuse dimension) | Frameworks don't address adversary use of public AI APIs for reconnaissance/attack | PROMPTFLUX, PROMPTSTEAL, phishing generation |
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 prompt injection RCE) | 9.6 | 91 | No | Yes — demonstrated | Yes (AI tooling enables) | Suspected |
170
- | CVE-2026-30615 (Windsurf MCP RCE) | 9.8 | 94 | No | Partial | No | Suspected |
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 |