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.
- package/README.md +341 -1018
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/cloud-controls/aws.json +10712 -0
- package/defaults/cloud-controls/azure.json +7201 -0
- package/defaults/cloud-controls/gcp.json +4061 -0
- package/defaults/control-catalog.json +24 -0
- package/defaults/security-policy.json +2 -2
- package/dist/ci/pr-gate.js +22 -5
- package/dist/cli/index.js +73 -2
- package/dist/cli/install.js +4 -55
- package/dist/cli/onboarding.js +18 -10
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +582 -15
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +955 -2
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +256 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/cloud-controls/apply.js +115 -0
- package/dist/gate/cloud-controls/bicep.js +36 -0
- package/dist/gate/cloud-controls/cfn.js +125 -0
- package/dist/gate/cloud-controls/detect.js +104 -0
- package/dist/gate/cloud-controls/hcl.js +140 -0
- package/dist/gate/cloud-controls/types.js +87 -0
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +202 -9
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +316 -130
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +131 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +161 -24
- package/dist/mcp/orchestration.js +377 -89
- package/dist/mcp/server.js +460 -69
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +37 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +56 -3
- package/dist/tests/run.js +124 -1
- package/package.json +9 -9
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +118 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +377 -0
- package/skills/ai-llm-redteam/SKILL.md +113 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
- package/skills/android-penetration-tester/SKILL.md +464 -46
- package/skills/anti-replay-tester/SKILL.md +115 -0
- package/skills/appsec-code-auditor/SKILL.md +94 -0
- package/skills/artifact-integrity-analyst/SKILL.md +450 -0
- package/skills/attack-navigator/SKILL.md +476 -8
- package/skills/auth-session-hacker/SKILL.md +111 -0
- package/skills/aws-penetration-tester/SKILL.md +510 -0
- package/skills/azure-penetration-tester/SKILL.md +542 -3
- package/skills/binary-auth-validator/SKILL.md +120 -0
- package/skills/bot-detection-specialist/SKILL.md +118 -0
- package/skills/business-logic-attacker/SKILL.md +240 -0
- package/skills/capec-code-mapper/SKILL.md +93 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
- package/skills/ciso-orchestrator/SKILL.md +465 -43
- package/skills/cloud-infra-specialist/SKILL.md +127 -0
- package/skills/compliance-gap-analyst/SKILL.md +431 -0
- package/skills/compliance-grc/SKILL.md +94 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +111 -0
- package/skills/crypto-pki-specialist/SKILL.md +96 -0
- package/skills/csa-ccm-mapper/SKILL.md +93 -0
- package/skills/csf2-governance-mapper/SKILL.md +93 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +118 -0
- package/skills/dependency-confusion-attacker/SKILL.md +424 -0
- package/skills/device-integrity-aggregator/SKILL.md +117 -0
- package/skills/dos-resilience-tester/SKILL.md +106 -0
- package/skills/dread-scorer/SKILL.md +93 -0
- package/skills/egress-policy-enforcer/SKILL.md +108 -0
- package/skills/evidence-collector/SKILL.md +107 -0
- package/skills/file-upload-attacker/SKILL.md +118 -0
- package/skills/gcp-penetration-tester/SKILL.md +510 -2
- package/skills/git-history-secret-scanner/SKILL.md +115 -0
- package/skills/gitops-delivery-auditor/SKILL.md +120 -0
- package/skills/iac-security-auditor/SKILL.md +125 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
- package/skills/incident-responder/SKILL.md +120 -0
- package/skills/injection-specialist/SKILL.md +111 -0
- package/skills/ios-security-auditor/SKILL.md +291 -0
- package/skills/json-ambiguity-tester/SKILL.md +145 -0
- package/skills/k8s-container-escaper/SKILL.md +406 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
- package/skills/kill-switch-engineer/SKILL.md +111 -0
- package/skills/linddun-privacy-analyst/SKILL.md +111 -0
- package/skills/logic-race-fuzzer/SKILL.md +452 -0
- package/skills/mobile-api-network-attacker/SKILL.md +430 -0
- package/skills/mobile-binary-hardener/SKILL.md +111 -0
- package/skills/mobile-security-specialist/SKILL.md +94 -0
- package/skills/mobile-webview-auditor/SKILL.md +105 -0
- package/skills/model-extraction-attacker/SKILL.md +228 -0
- package/skills/multipart-abuse-tester/SKILL.md +93 -0
- package/skills/oauth-pkce-specialist/SKILL.md +113 -0
- package/skills/parser-exhaustion-tester/SKILL.md +151 -0
- package/skills/pentest-infra/SKILL.md +107 -0
- package/skills/pentest-social/SKILL.md +210 -0
- package/skills/pentest-team/SKILL.md +96 -0
- package/skills/pentest-web-api/SKILL.md +107 -0
- package/skills/privacy-flow-analyst/SKILL.md +243 -0
- package/skills/prompt-injection-specialist/SKILL.md +403 -0
- package/skills/quantum-migration-planner/SKILL.md +105 -0
- package/skills/rag-poisoning-specialist/SKILL.md +367 -0
- package/skills/registry-mirror-enforcer/SKILL.md +93 -0
- package/skills/rotation-validation-agent/SKILL.md +121 -0
- package/skills/samm-assessor/SKILL.md +94 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
- package/skills/senior-security-engineer/SKILL.md +178 -0
- package/skills/serialization-memory-attacker/SKILL.md +341 -0
- package/skills/session-timeout-tester/SKILL.md +170 -0
- package/skills/slsa-level3-enforcer/SKILL.md +121 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
- package/skills/ssrf-detection-validator/SKILL.md +117 -0
- package/skills/step-up-auth-enforcer/SKILL.md +93 -0
- package/skills/stride-pasta-analyst/SKILL.md +429 -0
- package/skills/supply-chain-devsecops/SKILL.md +107 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
- package/skills/threat-modeler/SKILL.md +94 -0
- package/skills/tls-certificate-auditor/SKILL.md +582 -18
- package/skills/token-reuse-detector/SKILL.md +104 -0
- package/skills/trike-risk-modeler/SKILL.md +93 -0
- package/skills/unicode-homograph-tester/SKILL.md +93 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
- package/skills/webhook-security-tester/SKILL.md +111 -0
- package/skills/zero-trust-architect/SKILL.md +118 -0
package/dist/mcp/model-router.js
CHANGED
|
@@ -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
|
|
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
|
|
61
|
-
outputPer1M: 15
|
|
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.
|
|
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.
|
|
78
|
-
outputPer1M: 10
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
112
|
-
outputPer1M: 10
|
|
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
|
|
269
|
-
* @param health
|
|
270
|
-
* @param preferred
|
|
271
|
-
* @
|
|
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
|
|
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
|
|
362
|
-
const outputRate = model?.outputPer1M ?? (usage.tier === "haiku" ? 1.25 : 15
|
|
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.
|