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
|
@@ -417,6 +417,30 @@
|
|
|
417
417
|
"frameworks": ["OWASP LLM Top 10 2025", "NIST AI RMF"],
|
|
418
418
|
"evidence": ["human_in_loop_for_actions", "tool_allowlist_router"]
|
|
419
419
|
},
|
|
420
|
+
{
|
|
421
|
+
"id": "AI_AGENTIC_INSTRUCTION_INTEGRITY",
|
|
422
|
+
"description": "Agentic-instruction files (SKILL.md, .claude, CLAUDE.md, AGENTS.md, .cursorrules, .mcp.json, copilot-instructions) are scanned for prompt-override, exfiltration, tool-poisoning, persistence, hidden-character, and credential-harvest payloads before any AI agent ingests the repository.",
|
|
423
|
+
"automation": "tooling",
|
|
424
|
+
"surfaces": ["ai", "agentic"],
|
|
425
|
+
"frameworks": ["OWASP LLM Top 10 2025", "MITRE ATLAS"],
|
|
426
|
+
"required_scanners": []
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"id": "AI_LLM_ALGORITHMIC_FAIRNESS",
|
|
430
|
+
"description": "ML decision systems affecting people carry fairness/bias evaluation (disparate impact, equalized odds, demographic parity) and representativeness evidence.",
|
|
431
|
+
"automation": "evidence",
|
|
432
|
+
"surfaces": ["ai"],
|
|
433
|
+
"frameworks": ["EU AI Act", "NIST AI RMF", "ISO 42001"],
|
|
434
|
+
"evidence": ["fairness_evaluation", "bias_testing_present"]
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"id": "AI_SHADOW_AI_DATA_LEAKAGE",
|
|
438
|
+
"description": "Secrets and PII are redacted/tokenized before reaching any LLM payload, preventing shadow-AI data leakage to model providers.",
|
|
439
|
+
"automation": "evidence",
|
|
440
|
+
"surfaces": ["ai"],
|
|
441
|
+
"frameworks": ["NIST AI RMF", "OWASP LLM Top 10 2025"],
|
|
442
|
+
"evidence": ["pii_redaction_before_llm", "secret_dlp_guard"]
|
|
443
|
+
},
|
|
420
444
|
{
|
|
421
445
|
"id": "SLSA_L3_PROVENANCE",
|
|
422
446
|
"description": "Build artifacts have signed SLSA Level 3 provenance from a hermetic, ephemeral CI build.",
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"description": "Default security gate policy for security-mcp. Copy to .mcp/policies/security-policy.json and customize for your project.",
|
|
5
5
|
"required_checks": {
|
|
6
6
|
"secrets_scan": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
7
|
-
"dependency_scan": { "severity_block": ["CRITICAL"] },
|
|
8
|
-
"sast": { "severity_block": ["CRITICAL"] },
|
|
7
|
+
"dependency_scan": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
8
|
+
"sast": { "severity_block": ["HIGH", "CRITICAL"] },
|
|
9
9
|
"iac_scan": { "severity_block": ["HIGH", "CRITICAL"] }
|
|
10
10
|
},
|
|
11
11
|
"environments": {
|
package/dist/ci/pr-gate.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { runPrGate } from "../gate/policy.js";
|
|
2
4
|
// Allow safe git revision operators (~ and ^) plus ref/path characters. CWE-88.
|
|
3
5
|
const SAFE_REF_RE = /^[a-zA-Z0-9_./~^-]+$/;
|
|
@@ -24,7 +26,13 @@ function safeEnvTargets(envVar) {
|
|
|
24
26
|
return true;
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Run the policy gate using configuration from environment variables.
|
|
31
|
+
* Exported so the `security-mcp ci:pr-gate` CLI subcommand can invoke it,
|
|
32
|
+
* while `node dist/ci/pr-gate.js` (and `npm run ci:pr-gate`) still run it directly.
|
|
33
|
+
* Exits the process: code 2 when the gate fails, 0 when it passes.
|
|
34
|
+
*/
|
|
35
|
+
export async function runGateFromEnv() {
|
|
28
36
|
const baseRef = safeEnvRef("SECURITY_GATE_BASE_REF", "origin/main");
|
|
29
37
|
const headRef = safeEnvRef("SECURITY_GATE_HEAD_REF", "HEAD");
|
|
30
38
|
const policyPath = process.env.SECURITY_GATE_POLICY || ".mcp/policies/security-policy.json";
|
|
@@ -37,7 +45,16 @@ async function main() {
|
|
|
37
45
|
process.exit(2);
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
// Auto-run only when executed directly (node dist/ci/pr-gate.js / npm run ci:pr-gate),
|
|
49
|
+
// not when imported by the CLI dispatcher.
|
|
50
|
+
const invokedDirectly = process.argv[1] !== undefined &&
|
|
51
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
52
|
+
if (invokedDirectly) {
|
|
53
|
+
try {
|
|
54
|
+
await runGateFromEnv();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error("security gate crashed:", err);
|
|
58
|
+
process.exit(3);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { dirname, resolve } from "node:path";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { homedir, platform } from "node:os";
|
|
17
17
|
import { runInstall } from "./install.js";
|
|
18
18
|
import { main as runServer } from "../mcp/server.js";
|
|
19
19
|
import { notifyIfUpdateAvailable } from "./update.js";
|
|
20
|
+
import { autoHardenTree } from "../gate/cloud-controls/apply.js";
|
|
21
|
+
import { runGateFromEnv } from "../ci/pr-gate.js";
|
|
22
|
+
import { signPolicyFile } from "../gate/policy.js";
|
|
20
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
24
|
const require = createRequire(import.meta.url);
|
|
22
25
|
function getVersion() {
|
|
@@ -58,6 +61,9 @@ COMMANDS
|
|
|
58
61
|
install-global Install using the globally installed security-mcp binary
|
|
59
62
|
config Print MCP config JSON for manual editor setup
|
|
60
63
|
doctor Verify the installation is working correctly
|
|
64
|
+
autoharden Auto-apply FSBP/CIS hardening fixes to Terraform (use --dry-run to preview)
|
|
65
|
+
ci:pr-gate Run the policy gate against the current diff (for CI/pre-commit)
|
|
66
|
+
sign-policy Sign the policy file with SECURITY_POLICY_HMAC_KEY for tamper protection
|
|
61
67
|
|
|
62
68
|
OPTIONS (install)
|
|
63
69
|
--claude-code Write config for Claude Code only
|
|
@@ -93,6 +99,13 @@ EXAMPLES
|
|
|
93
99
|
# Verify installation health:
|
|
94
100
|
npx -y security-mcp@latest doctor
|
|
95
101
|
|
|
102
|
+
# Run the policy gate in CI (fails the build on HIGH/CRITICAL findings):
|
|
103
|
+
npx -y security-mcp@latest ci:pr-gate
|
|
104
|
+
|
|
105
|
+
# Sign the policy file so tampering is detected at gate startup:
|
|
106
|
+
export SECURITY_POLICY_HMAC_KEY="$(openssl rand -hex 32)"
|
|
107
|
+
npx -y security-mcp@latest sign-policy
|
|
108
|
+
|
|
96
109
|
# Print JSON config snippet:
|
|
97
110
|
npx -y security-mcp@latest config
|
|
98
111
|
security-mcp config --use-global-binary
|
|
@@ -179,6 +192,50 @@ function runDoctor() {
|
|
|
179
192
|
process.exit(1);
|
|
180
193
|
}
|
|
181
194
|
}
|
|
195
|
+
async function runAutoHarden(dryRun) {
|
|
196
|
+
const report = await autoHardenTree({ write: !dryRun });
|
|
197
|
+
const verb = dryRun ? "Would apply" : "Applied";
|
|
198
|
+
process.stdout.write(`\nsecurity-mcp autoharden v${VERSION}\n`);
|
|
199
|
+
process.stdout.write("=".repeat(40) + "\n\n");
|
|
200
|
+
process.stdout.write(`${verb} ${report.applied.length} fix(es) across ${report.filesChanged.length} file(s).\n`);
|
|
201
|
+
for (const fix of report.applied) {
|
|
202
|
+
process.stdout.write(` [FIX] ${fix.ruleId} ${fix.resource} (${fix.file})\n`);
|
|
203
|
+
}
|
|
204
|
+
for (const m of report.manual) {
|
|
205
|
+
process.stdout.write(` [MANUAL] ${m.ruleId} ${m.resource} (${m.file}) — ${m.reason}\n`);
|
|
206
|
+
if (m.snippet)
|
|
207
|
+
process.stdout.write(` ${m.snippet}\n`);
|
|
208
|
+
}
|
|
209
|
+
if (dryRun)
|
|
210
|
+
process.stdout.write("\nDry run — no files were modified. Re-run without --dry-run to apply.\n");
|
|
211
|
+
process.stdout.write("\n");
|
|
212
|
+
}
|
|
213
|
+
// Minimum HMAC key length, mirrors POLICY_HMAC_MIN_KEY_BYTES in src/gate/policy.ts.
|
|
214
|
+
const POLICY_HMAC_MIN_KEY_BYTES = 32;
|
|
215
|
+
function runSignPolicy() {
|
|
216
|
+
const key = process.env["SECURITY_POLICY_HMAC_KEY"];
|
|
217
|
+
if (!key || Buffer.byteLength(key, "utf-8") < POLICY_HMAC_MIN_KEY_BYTES) {
|
|
218
|
+
process.stderr.write(`Error: SECURITY_POLICY_HMAC_KEY must be set and at least ${POLICY_HMAC_MIN_KEY_BYTES} bytes.\n` +
|
|
219
|
+
"Generate one with: openssl rand -hex 32\n");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const policyPath = process.env["SECURITY_GATE_POLICY"] || ".mcp/policies/security-policy.json";
|
|
223
|
+
if (!existsSync(policyPath)) {
|
|
224
|
+
process.stderr.write(`Error: policy file not found at "${policyPath}".\n` +
|
|
225
|
+
"Create one first (cp node_modules/security-mcp/defaults/security-policy.json .mcp/policies/), " +
|
|
226
|
+
"or set SECURITY_GATE_POLICY to its path.\n");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const raw = readFileSync(policyPath, "utf-8");
|
|
230
|
+
const signature = signPolicyFile(raw, key);
|
|
231
|
+
// 0o600 — keep the sidecar non-world-readable, consistent with data-at-rest hardening.
|
|
232
|
+
writeFileSync(`${policyPath}.hmac`, signature + "\n", { mode: 0o600 });
|
|
233
|
+
process.stdout.write(`\nsecurity-mcp sign-policy v${VERSION}\n`);
|
|
234
|
+
process.stdout.write("=".repeat(40) + "\n\n");
|
|
235
|
+
process.stdout.write(` [SIGNED] ${policyPath}\n`);
|
|
236
|
+
process.stdout.write(` [WROTE] ${policyPath}.hmac\n\n`);
|
|
237
|
+
process.stdout.write("Commit both files so CI can verify policy integrity at gate startup.\n\n");
|
|
238
|
+
}
|
|
182
239
|
async function main() {
|
|
183
240
|
const args = process.argv.slice(2);
|
|
184
241
|
const useGlobalBinary = args.includes("--use-global-binary");
|
|
@@ -191,7 +248,8 @@ async function main() {
|
|
|
191
248
|
process.exit(0);
|
|
192
249
|
}
|
|
193
250
|
const command = args[0] ?? "serve";
|
|
194
|
-
if (command === "serve") {
|
|
251
|
+
if (command === "serve" || command === "ci:pr-gate") {
|
|
252
|
+
// Non-blocking: keep stdout reserved for protocol/JSON output.
|
|
195
253
|
void notifyIfUpdateAvailable(VERSION);
|
|
196
254
|
}
|
|
197
255
|
else {
|
|
@@ -245,6 +303,19 @@ async function main() {
|
|
|
245
303
|
runDoctor();
|
|
246
304
|
break;
|
|
247
305
|
}
|
|
306
|
+
case "autoharden": {
|
|
307
|
+
await runAutoHarden(args.includes("--dry-run"));
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "ci:pr-gate": {
|
|
311
|
+
// Reads SECURITY_GATE_* env vars; exits non-zero when the gate fails.
|
|
312
|
+
await runGateFromEnv();
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "sign-policy": {
|
|
316
|
+
runSignPolicy();
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
248
319
|
default: {
|
|
249
320
|
process.stderr.write(`Unknown command: ${command}\nRun with --help for usage.\n`);
|
|
250
321
|
process.exit(1);
|
package/dist/cli/install.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { homedir, platform } from "node:os";
|
|
9
|
-
import * as https from "node:https";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
11
10
|
import { runOnboarding, installSecurityTools, commandExists, SECURITY_TOOLS } from "./onboarding.js";
|
|
12
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -172,60 +171,10 @@ function installSkill(dryRun) {
|
|
|
172
171
|
* Mirrors the same pattern used for security tool binary downloads in onboarding.ts.
|
|
173
172
|
*/
|
|
174
173
|
// CWE-22: only alphanumeric, hyphens, and dots allowed in skill names
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!SAFE_SKILL_NAME_RE.test(skillName)) {
|
|
180
|
-
process.stdout.write(` [error] invalid skill name "${skillName}" — skipping download\n`);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const { hostname } = new URL(url);
|
|
185
|
-
if (!ALLOWED_SKILL_HOSTS.has(hostname)) {
|
|
186
|
-
process.stdout.write(` [error] blocked skill download from unauthorized host "${hostname}"\n`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
process.stdout.write(` [error] invalid skill URL "${url}" — skipping download\n`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const skillDest = resolveHome(`~/.claude/skills/${skillName}/SKILL.md`);
|
|
195
|
-
if (dryRun) {
|
|
196
|
-
process.stdout.write(` [dry-run] would download skill "${skillName}" from ${url} → ${skillDest}\n`);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const MAX_SKILL_BYTES = 512 * 1024; // 512 KB — skills are markdown files
|
|
200
|
-
const content = await new Promise((resolve) => {
|
|
201
|
-
const req = https.get(url, { headers: { "User-Agent": "security-mcp" } }, (res) => {
|
|
202
|
-
if ((res.statusCode ?? 500) >= 400) {
|
|
203
|
-
res.resume();
|
|
204
|
-
resolve(null);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
let body = "";
|
|
208
|
-
res.setEncoding("utf8");
|
|
209
|
-
res.on("data", (chunk) => {
|
|
210
|
-
body += chunk;
|
|
211
|
-
if (Buffer.byteLength(body, "utf8") > MAX_SKILL_BYTES) {
|
|
212
|
-
req.destroy();
|
|
213
|
-
resolve(null);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
res.on("end", () => resolve(body));
|
|
217
|
-
});
|
|
218
|
-
req.on("error", () => resolve(null));
|
|
219
|
-
req.setTimeout(10000, () => { req.destroy(); resolve(null); });
|
|
220
|
-
});
|
|
221
|
-
if (!content) {
|
|
222
|
-
process.stdout.write(` [error] failed to download skill "${skillName}" from ${url}\n`);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
mkdirSync(dirname(skillDest), { recursive: true });
|
|
226
|
-
writeFileSync(skillDest, content, "utf-8");
|
|
227
|
-
process.stdout.write(` installed skill: ${skillDest}\n`);
|
|
228
|
-
}
|
|
174
|
+
// REMOVED downloadSkill(): an unused, integrity-free network skill installer
|
|
175
|
+
// (no sha256, no content sanitization) that, if ever wired up, would bypass every
|
|
176
|
+
// protection in orchestration.ensureSkill. Skills are bundled in the package and
|
|
177
|
+
// resolved locally by ensureSkill; there is no need for an unauthenticated fetcher.
|
|
229
178
|
/**
|
|
230
179
|
* Eagerly install the orchestrator skill (bundled in the package) plus record
|
|
231
180
|
* its version so orchestration.ensure_skill can detect future updates.
|
package/dist/cli/onboarding.js
CHANGED
|
@@ -340,11 +340,21 @@ async function installFromGitHub(tool, os) {
|
|
|
340
340
|
print(` Integrity verified (SHA-256 matched).`);
|
|
341
341
|
}
|
|
342
342
|
else {
|
|
343
|
-
print(`
|
|
343
|
+
print(` ABORT: checksum file found but no entry for ${fileName} — refusing to install an unverified binary (CWE-494).`);
|
|
344
|
+
try {
|
|
345
|
+
unlinkSync(tmpFile);
|
|
346
|
+
}
|
|
347
|
+
catch { /* ignore cleanup failure */ }
|
|
348
|
+
return false;
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
351
|
else {
|
|
347
|
-
print(`
|
|
352
|
+
print(` ABORT: no checksum file in release assets — refusing to install an unverified binary (CWE-494).`);
|
|
353
|
+
try {
|
|
354
|
+
unlinkSync(tmpFile);
|
|
355
|
+
}
|
|
356
|
+
catch { /* ignore cleanup failure */ }
|
|
357
|
+
return false;
|
|
348
358
|
}
|
|
349
359
|
const destDir = "/usr/local/bin";
|
|
350
360
|
if (tool.tarball) {
|
|
@@ -432,12 +442,10 @@ async function tryDnf(tool) {
|
|
|
432
442
|
print(` sudo ${mgr} install -y trivy`);
|
|
433
443
|
return run("sudo", [mgr, "install", "-y", "trivy"]);
|
|
434
444
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return run("bash", ["-c", tool.installScript]);
|
|
440
|
-
}
|
|
445
|
+
// REMOVED (CWE-78/CWE-494): the `curl … | sudo sh` install-script strategy piped an
|
|
446
|
+
// unpinned, live-fetched script into root with no checksum — a compromise of the upstream
|
|
447
|
+
// repo or a MITM yielded root RCE on `security-mcp install`. Tools are now installed only
|
|
448
|
+
// via OS package managers or the checksum-verified GitHub-release path (installFromGitHub).
|
|
441
449
|
async function tryWinget(tool) {
|
|
442
450
|
if (!tool.winget || !commandExists("winget"))
|
|
443
451
|
return false;
|
|
@@ -461,10 +469,10 @@ async function installSingleTool(tool, os) {
|
|
|
461
469
|
print(`\n Installing ${tool.displayName}...`);
|
|
462
470
|
const strategies = [];
|
|
463
471
|
if (os === "macos") {
|
|
464
|
-
strategies.push(() => tryBrew(tool), () => tryPip(tool), () => tryGoInstall(tool), () =>
|
|
472
|
+
strategies.push(() => tryBrew(tool), () => tryPip(tool), () => tryGoInstall(tool), () => installFromGitHub(tool, os));
|
|
465
473
|
}
|
|
466
474
|
else if (os === "linux") {
|
|
467
|
-
strategies.push(() => tryApt(tool), () => tryDnf(tool), () => tryPip(tool), () => tryGoInstall(tool), () =>
|
|
475
|
+
strategies.push(() => tryApt(tool), () => tryDnf(tool), () => tryPip(tool), () => tryGoInstall(tool), () => installFromGitHub(tool, os));
|
|
468
476
|
}
|
|
469
477
|
else {
|
|
470
478
|
// Windows
|
package/dist/gate/baseline.js
CHANGED
|
@@ -5,7 +5,39 @@
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
9
|
import { join } from "node:path";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// HMAC integrity helpers — TM-013 fix
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// HMAC-SHA256 requires at least 32 bytes (256 bits) per NIST SP 800-107 §5.3.4.
|
|
14
|
+
const HMAC_MIN_KEY_BYTES = 32;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the HMAC key from env, or null if not configured.
|
|
17
|
+
* Throws if the key is present but too short.
|
|
18
|
+
*/
|
|
19
|
+
function getHmacKey() {
|
|
20
|
+
const key = process.env["SECURITY_POLICY_HMAC_KEY"];
|
|
21
|
+
if (!key)
|
|
22
|
+
return null;
|
|
23
|
+
if (Buffer.byteLength(key, "utf-8") < HMAC_MIN_KEY_BYTES) {
|
|
24
|
+
throw new Error(`SECURITY_POLICY_HMAC_KEY is too short (${Buffer.byteLength(key, "utf-8")} bytes). ` +
|
|
25
|
+
`Provide at least ${HMAC_MIN_KEY_BYTES} bytes — generate one with: ` +
|
|
26
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
|
|
27
|
+
}
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
function signBaseline(json, key) {
|
|
31
|
+
return createHmac("sha256", key).update(json, "utf-8").digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function verifyBaselineHmac(json, stored, key) {
|
|
34
|
+
const expected = createHmac("sha256", key).update(json, "utf-8").digest("hex");
|
|
35
|
+
const storedBuf = Buffer.from(stored, "hex");
|
|
36
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
37
|
+
if (storedBuf.length !== expectedBuf.length)
|
|
38
|
+
return false;
|
|
39
|
+
return timingSafeEqual(storedBuf, expectedBuf);
|
|
40
|
+
}
|
|
9
41
|
const execFileAsync = promisify(execFile);
|
|
10
42
|
const BASELINE_DIR = join(process.cwd(), ".mcp", "baselines");
|
|
11
43
|
async function ensureDir(dir) {
|
|
@@ -32,37 +64,53 @@ export async function getCommitHash() {
|
|
|
32
64
|
/**
|
|
33
65
|
* Saves a gate result as baseline for the given commit hash.
|
|
34
66
|
* Also updates the latest baseline copy.
|
|
67
|
+
*
|
|
68
|
+
* TM-013 fix: When SECURITY_POLICY_HMAC_KEY is set, the serialised payload is
|
|
69
|
+
* HMAC-SHA256 signed and the signature is stored in the envelope. Unsigned
|
|
70
|
+
* writes are still permitted when no key is configured (graceful degradation),
|
|
71
|
+
* but loadBaseline will reject a previously-signed file whose signature no
|
|
72
|
+
* longer matches (tamper detection).
|
|
35
73
|
*/
|
|
36
74
|
export async function saveBaseline(runId, result, commitHash) {
|
|
37
75
|
await ensureDir(BASELINE_DIR);
|
|
38
76
|
const payload = { runId, commitHash, savedAt: new Date().toISOString(), result };
|
|
39
77
|
const json = JSON.stringify(payload, null, 2);
|
|
78
|
+
// Sign if a key is available
|
|
79
|
+
const hmacKey = getHmacKey();
|
|
80
|
+
const envelope = hmacKey
|
|
81
|
+
? JSON.stringify({ payload, hmacSha256: signBaseline(json, hmacKey) }, null, 2)
|
|
82
|
+
: json;
|
|
40
83
|
// Write to temp file then rename (atomic)
|
|
41
84
|
const safehash = commitHash.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
42
85
|
const targetPath = join(BASELINE_DIR, `${safehash}.json`);
|
|
43
86
|
const latestPath = join(BASELINE_DIR, "latest.json");
|
|
44
|
-
const tmpPath = `${targetPath}.tmp`;
|
|
87
|
+
const tmpPath = `${targetPath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
45
88
|
try {
|
|
46
|
-
await writeFile(tmpPath,
|
|
89
|
+
await writeFile(tmpPath, envelope, "utf-8");
|
|
47
90
|
await rename(tmpPath, targetPath);
|
|
48
91
|
}
|
|
49
92
|
catch {
|
|
50
93
|
// fallback: write directly
|
|
51
|
-
await writeFile(targetPath,
|
|
94
|
+
await writeFile(targetPath, envelope, "utf-8").catch(() => { });
|
|
52
95
|
}
|
|
53
96
|
// Update latest (best-effort atomic)
|
|
54
|
-
const latestTmp = `${latestPath}.tmp`;
|
|
97
|
+
const latestTmp = `${latestPath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
55
98
|
try {
|
|
56
|
-
await writeFile(latestTmp,
|
|
99
|
+
await writeFile(latestTmp, envelope, "utf-8");
|
|
57
100
|
await rename(latestTmp, latestPath);
|
|
58
101
|
}
|
|
59
102
|
catch {
|
|
60
|
-
await writeFile(latestPath,
|
|
103
|
+
await writeFile(latestPath, envelope, "utf-8").catch(() => { });
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
/**
|
|
64
107
|
* Loads a baseline by commit hash, or the latest baseline if no hash given.
|
|
65
108
|
* Returns null if no baseline exists or it's corrupted.
|
|
109
|
+
*
|
|
110
|
+
* TM-013 fix: If the file is stored in the HMAC envelope format AND
|
|
111
|
+
* SECURITY_POLICY_HMAC_KEY is configured, the HMAC is verified before the
|
|
112
|
+
* payload is returned. A tampered baseline (missing or wrong HMAC) is
|
|
113
|
+
* rejected — the gate will run without a baseline rather than trust forged data.
|
|
66
114
|
*/
|
|
67
115
|
export async function loadBaseline(commitHash) {
|
|
68
116
|
await ensureDir(BASELINE_DIR);
|
|
@@ -76,7 +124,34 @@ export async function loadBaseline(commitHash) {
|
|
|
76
124
|
}
|
|
77
125
|
try {
|
|
78
126
|
const raw = await readFile(filePath, "utf-8");
|
|
79
|
-
const
|
|
127
|
+
const top = JSON.parse(raw);
|
|
128
|
+
// Detect envelope format (has both "payload" and "hmacSha256")
|
|
129
|
+
if ("payload" in top && "hmacSha256" in top) {
|
|
130
|
+
const envelope = top;
|
|
131
|
+
const hmacKey = getHmacKey();
|
|
132
|
+
if (hmacKey) {
|
|
133
|
+
// Re-serialise the inner payload the same way saveBaseline did
|
|
134
|
+
const expectedInput = JSON.stringify(envelope.payload, null, 2);
|
|
135
|
+
if (!verifyBaselineHmac(expectedInput, envelope.hmacSha256, hmacKey)) {
|
|
136
|
+
console.error("[baseline] HMAC verification failed — baseline may have been tampered. Ignoring.");
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Key not configured: we can't verify, but we can warn
|
|
142
|
+
console.warn("[baseline] Baseline is signed but SECURITY_POLICY_HMAC_KEY is not set — skipping HMAC verification.");
|
|
143
|
+
}
|
|
144
|
+
return envelope.payload.result ?? null;
|
|
145
|
+
}
|
|
146
|
+
// Legacy format (unsigned) — parse directly
|
|
147
|
+
const parsed = top;
|
|
148
|
+
const hmacKey = getHmacKey();
|
|
149
|
+
if (hmacKey) {
|
|
150
|
+
// A key is configured but the file is unsigned — reject it to prevent
|
|
151
|
+
// an attacker from stripping the HMAC wrapper to bypass verification.
|
|
152
|
+
console.error("[baseline] SECURITY_POLICY_HMAC_KEY is set but baseline is unsigned — ignoring to prevent tampering bypass.");
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
80
155
|
return parsed.result ?? null;
|
|
81
156
|
}
|
|
82
157
|
catch {
|
package/dist/gate/catalog.js
CHANGED
|
@@ -23,8 +23,16 @@ async function readJsonWithFallback(relPath, fallbackName) {
|
|
|
23
23
|
".mcp/catalog/control-catalog.json": "SECURITY_GATE_CONTROL_CATALOG"
|
|
24
24
|
};
|
|
25
25
|
const overrideEnv = overrideEnvMap[relPath];
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const overridePath = overrideEnv ? process.env[overrideEnv] : undefined;
|
|
27
|
+
if (overridePath) {
|
|
28
|
+
// Guard against path traversal (VULN-003 / CWE-22): resolve() + startsWith() is required;
|
|
29
|
+
// join() alone normalises '..' but does not prevent escape from the project directory.
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const resolved = resolve(cwd, overridePath);
|
|
32
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + "/")) {
|
|
33
|
+
throw new Error(`${overrideEnv} path escapes the project directory`);
|
|
34
|
+
}
|
|
35
|
+
return await readFile(resolved, "utf-8");
|
|
28
36
|
}
|
|
29
37
|
try {
|
|
30
38
|
return await readFile(join(process.cwd(), relPath), "utf-8");
|