security-mcp 1.1.4 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +341 -1018
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -35,9 +35,28 @@ const MEMORY_DIR = join(".mcp", "memory");
35
35
  const USAGE_FILE = join(MEMORY_DIR, "model-usage.json");
36
36
  const HEALTH_FILE = join(MEMORY_DIR, "provider-health.json");
37
37
  const POLICY_FILE = join(".mcp", "policies", "security-policy.json");
38
- const DEFAULT_BUDGET_USD = 5.0;
38
+ const DEFAULT_BUDGET_USD = 5;
39
39
  const CIRCUIT_BREAKER_THRESHOLD = 3; // failures before circuit opens
40
40
  const CIRCUIT_BREAKER_COOLDOWN_MS = 60_000; // 60 seconds
41
+ // ---------------------------------------------------------------------------
42
+ // Rate limiting — recordProviderFailure to prevent circuit-breaker manipulation
43
+ // ---------------------------------------------------------------------------
44
+ const _providerFailureSubmissions = new Map();
45
+ const FAILURE_RATE_LIMIT = 5; // max 5 failure reports per provider per window
46
+ const FAILURE_WINDOW_MS = 300_000; // 5 minute window
47
+ export function recordProviderFailureRateLimited(providerName) {
48
+ const now = Date.now();
49
+ const entry = _providerFailureSubmissions.get(providerName);
50
+ if (!entry || now - entry.windowStart > FAILURE_WINDOW_MS) {
51
+ _providerFailureSubmissions.set(providerName, { count: 1, windowStart: now });
52
+ return { allowed: true };
53
+ }
54
+ if (entry.count >= FAILURE_RATE_LIMIT) {
55
+ return { allowed: false, reason: `Rate limit exceeded: max ${FAILURE_RATE_LIMIT} failure reports per provider per 5 minutes` };
56
+ }
57
+ entry.count++;
58
+ return { allowed: true };
59
+ }
41
60
  /**
42
61
  * Full model registry across all providers.
43
62
  * Pricing sourced from public pricing pages (approximate, for routing decisions only).
@@ -57,8 +76,8 @@ export const MODEL_REGISTRY = [
57
76
  modelId: "claude-sonnet-4-6",
58
77
  provider: "anthropic",
59
78
  capabilityTier: "standard",
60
- inputPer1M: 3.0,
61
- outputPer1M: 15.0,
79
+ inputPer1M: 3,
80
+ outputPer1M: 15,
62
81
  label: "Claude Sonnet 4.6"
63
82
  },
64
83
  // OpenAI — GPT
@@ -67,15 +86,15 @@ export const MODEL_REGISTRY = [
67
86
  provider: "openai",
68
87
  capabilityTier: "light",
69
88
  inputPer1M: 0.15,
70
- outputPer1M: 0.60,
89
+ outputPer1M: 0.6,
71
90
  label: "GPT-4o Mini"
72
91
  },
73
92
  {
74
93
  modelId: "gpt-4o",
75
94
  provider: "openai",
76
95
  capabilityTier: "standard",
77
- inputPer1M: 2.50,
78
- outputPer1M: 10.0,
96
+ inputPer1M: 2.5,
97
+ outputPer1M: 10,
79
98
  label: "GPT-4o"
80
99
  },
81
100
  // Google — Gemini
@@ -84,7 +103,7 @@ export const MODEL_REGISTRY = [
84
103
  provider: "google",
85
104
  capabilityTier: "light",
86
105
  inputPer1M: 0.075,
87
- outputPer1M: 0.30,
106
+ outputPer1M: 0.3,
88
107
  label: "Gemini 1.5 Flash"
89
108
  },
90
109
  {
@@ -92,7 +111,7 @@ export const MODEL_REGISTRY = [
92
111
  provider: "google",
93
112
  capabilityTier: "standard",
94
113
  inputPer1M: 1.25,
95
- outputPer1M: 5.0,
114
+ outputPer1M: 5,
96
115
  label: "Gemini 1.5 Pro"
97
116
  },
98
117
  // Cohere — Command R
@@ -101,17 +120,44 @@ export const MODEL_REGISTRY = [
101
120
  provider: "cohere",
102
121
  capabilityTier: "light",
103
122
  inputPer1M: 0.15,
104
- outputPer1M: 0.60,
123
+ outputPer1M: 0.6,
105
124
  label: "Command R"
106
125
  },
107
126
  {
108
127
  modelId: "command-r-plus",
109
128
  provider: "cohere",
110
129
  capabilityTier: "standard",
111
- inputPer1M: 2.50,
112
- outputPer1M: 10.0,
130
+ inputPer1M: 2.5,
131
+ outputPer1M: 10,
113
132
  label: "Command R+"
114
133
  },
134
+ // Anthropic — Claude Opus (advanced tier, opt-in via advanced_task_preference in policy)
135
+ {
136
+ modelId: "claude-opus-4-8",
137
+ provider: "anthropic",
138
+ capabilityTier: "advanced",
139
+ inputPer1M: 15,
140
+ outputPer1M: 75,
141
+ label: "Claude Opus 4.8"
142
+ },
143
+ // OpenAI — o1 (advanced tier)
144
+ {
145
+ modelId: "o1",
146
+ provider: "openai",
147
+ capabilityTier: "advanced",
148
+ inputPer1M: 15,
149
+ outputPer1M: 60,
150
+ label: "OpenAI o1"
151
+ },
152
+ // Google — Gemini 2.0 Flash (advanced tier)
153
+ {
154
+ modelId: "gemini-2.0-flash-thinking-exp",
155
+ provider: "google",
156
+ capabilityTier: "advanced",
157
+ inputPer1M: 0,
158
+ outputPer1M: 0,
159
+ label: "Gemini 2.0 Flash Thinking (experimental)"
160
+ },
115
161
  // Local — Ollama (zero cost, requires Ollama at localhost:11434)
116
162
  {
117
163
  modelId: "llama3",
@@ -185,7 +231,7 @@ export const TASK_TIER_MAP = {
185
231
  // Storage helpers
186
232
  // ---------------------------------------------------------------------------
187
233
  async function ensureMemoryDir() {
188
- await mkdir(MEMORY_DIR, { recursive: true });
234
+ await mkdir(MEMORY_DIR, { recursive: true, mode: 0o700 });
189
235
  }
190
236
  async function loadUsageStore() {
191
237
  try {
@@ -199,7 +245,7 @@ async function loadUsageStore() {
199
245
  async function saveUsageStore(store) {
200
246
  await ensureMemoryDir();
201
247
  store.updatedAt = new Date().toISOString();
202
- await writeFile(USAGE_FILE, JSON.stringify(store, null, 2) + "\n", "utf-8");
248
+ await writeFile(USAGE_FILE, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
203
249
  }
204
250
  async function loadHealthStore() {
205
251
  try {
@@ -213,7 +259,7 @@ async function loadHealthStore() {
213
259
  async function saveHealthStore(store) {
214
260
  await ensureMemoryDir();
215
261
  store.updatedAt = new Date().toISOString();
216
- await writeFile(HEALTH_FILE, JSON.stringify(store, null, 2) + "\n", "utf-8");
262
+ await writeFile(HEALTH_FILE, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
217
263
  }
218
264
  async function loadMaxBudget() {
219
265
  try {
@@ -235,6 +281,16 @@ async function loadPreferredProviders() {
235
281
  return null;
236
282
  }
237
283
  }
284
+ async function loadAdvancedTaskPreferences() {
285
+ try {
286
+ const raw = await readFile(POLICY_FILE, "utf-8");
287
+ const policy = JSON.parse(raw);
288
+ return policy.model_budget?.advanced_task_preference ?? [];
289
+ }
290
+ catch {
291
+ return [];
292
+ }
293
+ }
238
294
  // ---------------------------------------------------------------------------
239
295
  // Circuit breaker helpers
240
296
  // ---------------------------------------------------------------------------
@@ -265,12 +321,31 @@ function legacyTier(capTier) {
265
321
  * Select the cheapest healthy model that meets the capability requirement for
266
322
  * the given task type. Respects preferred_providers policy and circuit breakers.
267
323
  *
268
- * @param requiredTier Minimum capability tier for the task.
269
- * @param health Current provider health store.
270
- * @param preferred Optional ordered list of preferred providers.
271
- * @returns [chosen model, failoverUsed]
324
+ * @param requiredTier Minimum capability tier for the task.
325
+ * @param health Current provider health store.
326
+ * @param preferred Optional ordered list of preferred providers.
327
+ * @param preferAdvanced If true, try advanced-tier models first, fall back to standard.
328
+ * @returns [chosen model, failoverUsed]
272
329
  */
273
- function selectModel(requiredTier, health, preferred) {
330
+ function selectModel(requiredTier, health, preferred, preferAdvanced = false) {
331
+ // If advanced is preferred, try advanced-tier models first. Fall back gracefully to
332
+ // standard if none are healthy or registered — zero impact for users without Opus/o1.
333
+ if (preferAdvanced) {
334
+ const advancedCandidates = MODEL_REGISTRY.filter((m) => m.capabilityTier === "advanced");
335
+ const healthyAdvanced = advancedCandidates.filter((m) => !isCircuitOpen(health.providers[m.provider]));
336
+ if (healthyAdvanced.length > 0) {
337
+ const pool = preferred
338
+ ? [
339
+ ...healthyAdvanced.filter((m) => preferred.includes(m.provider)),
340
+ ...healthyAdvanced.filter((m) => !preferred.includes(m.provider))
341
+ ]
342
+ : healthyAdvanced;
343
+ pool.sort((a, b) => combinedCost(a) - combinedCost(b));
344
+ if (pool.length > 0)
345
+ return [pool[0], false];
346
+ }
347
+ // No advanced model available — fall through to standard selection silently.
348
+ }
274
349
  // Candidates: all models meeting the capability floor.
275
350
  const candidates = MODEL_REGISTRY.filter((m) => meetsCapabilityFloor(m, requiredTier));
276
351
  // Separate healthy vs. circuit-open providers.
@@ -304,14 +379,16 @@ function selectModel(requiredTier, health, preferred) {
304
379
  * Falls back to next-cheapest provider on circuit breaker open.
305
380
  */
306
381
  export async function getModelForTask(taskType, _opts) {
307
- const [store, health, maxBudget, preferred] = await Promise.all([
382
+ const [store, health, maxBudget, preferred, advancedPrefs] = await Promise.all([
308
383
  loadUsageStore(),
309
384
  loadHealthStore(),
310
385
  loadMaxBudget(),
311
- loadPreferredProviders()
386
+ loadPreferredProviders(),
387
+ loadAdvancedTaskPreferences()
312
388
  ]);
313
389
  const requiredTier = TASK_CAPABILITY_MAP[taskType];
314
- const [chosen, failoverUsed] = selectModel(requiredTier, health, preferred);
390
+ const preferAdvanced = advancedPrefs.includes(taskType);
391
+ const [chosen, failoverUsed] = selectModel(requiredTier, health, preferred, preferAdvanced);
315
392
  const spent = store.totalSpentUsd;
316
393
  const remaining = maxBudget - spent;
317
394
  const utilizationPct = maxBudget > 0 ? (spent / maxBudget) * 100 : 0;
@@ -326,6 +403,39 @@ export async function getModelForTask(taskType, _opts) {
326
403
  budgetStatus = "ok";
327
404
  }
328
405
  const rationale = buildRationale(taskType, requiredTier, chosen, failoverUsed, preferred);
406
+ // Determine whether all providers were circuit-open (best-effort fallback path).
407
+ const allProviders = ["anthropic", "openai", "google", "cohere", "local"];
408
+ const allCircuitsOpen = allProviders.every((p) => isCircuitOpen(health.providers[p]));
409
+ // ISO 42001 §9.1 — emit structured audit log for every routing decision.
410
+ let routingReason;
411
+ if (allCircuitsOpen) {
412
+ routingReason = "circuit_open_fallback";
413
+ }
414
+ else if (failoverUsed) {
415
+ routingReason = "capability_match";
416
+ }
417
+ else {
418
+ routingReason = "cost_optimized";
419
+ }
420
+ console.log(JSON.stringify({
421
+ event: "MODEL_ROUTING_DECISION",
422
+ timestamp: new Date().toISOString(),
423
+ taskType,
424
+ selectedModel: chosen.modelId,
425
+ selectedProvider: chosen.provider,
426
+ reason: routingReason,
427
+ circuitState: allCircuitsOpen ? "FALLBACK" : "NORMAL",
428
+ }));
429
+ // Additional high-severity audit entry for the circuit-breaker fallback path.
430
+ if (allCircuitsOpen) {
431
+ console.warn(JSON.stringify({
432
+ event: "MODEL_ROUTING_CIRCUIT_FALLBACK",
433
+ timestamp: new Date().toISOString(),
434
+ reason: "ALL_PROVIDERS_CIRCUIT_OPEN",
435
+ fallbackModel: chosen.modelId,
436
+ severity: "HIGH",
437
+ }));
438
+ }
329
439
  return {
330
440
  model: chosen.modelId,
331
441
  provider: chosen.provider,
@@ -358,8 +468,8 @@ function buildRationale(taskType, required, chosen, failoverUsed, preferred) {
358
468
  export async function trackUsage(usage) {
359
469
  const [store, health] = await Promise.all([loadUsageStore(), loadHealthStore()]);
360
470
  const model = MODEL_REGISTRY.find((m) => m.modelId === usage.model);
361
- const inputRate = model?.inputPer1M ?? (usage.tier === "haiku" ? 0.25 : 3.0);
362
- const outputRate = model?.outputPer1M ?? (usage.tier === "haiku" ? 1.25 : 15.0);
471
+ const inputRate = model?.inputPer1M ?? (usage.tier === "haiku" ? 0.25 : 3);
472
+ const outputRate = model?.outputPer1M ?? (usage.tier === "haiku" ? 1.25 : 15);
363
473
  const estimatedCost = (usage.inputTokens / 1_000_000) * inputRate +
364
474
  (usage.outputTokens / 1_000_000) * outputRate;
365
475
  const record = {
@@ -389,8 +499,13 @@ export async function trackUsage(usage) {
389
499
  /**
390
500
  * Record a provider failure (connection error, rate limit, auth failure).
391
501
  * Opens circuit breaker after CIRCUIT_BREAKER_THRESHOLD consecutive failures.
502
+ * Rate-limited to prevent deliberate circuit-breaker manipulation (max 5 per provider per 5 min).
392
503
  */
393
504
  export async function recordProviderFailure(provider) {
505
+ const rateCheck = recordProviderFailureRateLimited(provider);
506
+ if (!rateCheck.allowed) {
507
+ return { recorded: false, reason: rateCheck.reason };
508
+ }
394
509
  const health = await loadHealthStore();
395
510
  const now = new Date();
396
511
  const state = health.providers[provider] ?? {
@@ -407,6 +522,28 @@ export async function recordProviderFailure(provider) {
407
522
  }
408
523
  health.providers[provider] = state;
409
524
  await saveHealthStore(health);
525
+ // Circuit-state audit: warn and emit structured audit record if all known providers are circuit-open.
526
+ // Deliberate manipulation requires only CIRCUIT_BREAKER_THRESHOLD (3) failures per provider × 5 providers
527
+ // = 15 calls, constrained to max 5 per provider per 5-min window. Log at ERROR level so SIEM picks this up.
528
+ // MITRE ATLAS AML.T0040 (ML Model Inference API) — circuit-breaker exhaustion attack.
529
+ const allProviders = ["anthropic", "openai", "google", "cohere", "local"];
530
+ const allProvidersDown = allProviders.every((p) => isCircuitOpen(health.providers[p]));
531
+ if (allProvidersDown) {
532
+ // Determine which fallback model will be used (cheapest in registry, circuit ignored).
533
+ const fallbackCandidates = MODEL_REGISTRY.filter((m) => m.provider === "anthropic" && m.capabilityTier === "standard");
534
+ const fallbackModel = fallbackCandidates[0]?.modelId ?? "unknown";
535
+ console.error(JSON.stringify({
536
+ severity: "CRITICAL",
537
+ event: "ALL_PROVIDERS_CIRCUIT_OPEN",
538
+ message: "All AI providers are circuit-open. Routing to fallback model. This may indicate deliberate circuit-breaker manipulation.",
539
+ fallbackModel,
540
+ timestamp: new Date().toISOString(),
541
+ failingProvider: provider,
542
+ mitre: "AML.T0040",
543
+ action: "Manual investigation required. Call security.reset_provider_circuit after confirming provider health."
544
+ }));
545
+ }
546
+ return { recorded: true };
410
547
  }
411
548
  /**
412
549
  * Return health status for all providers — circuit breaker state and call counts.