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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tool-call structured audit log.
|
|
3
|
+
*
|
|
4
|
+
* Every MCP tool invocation is recorded as one structured JSONL line — the
|
|
5
|
+
* "one log per tool call, not per session" requirement for agentic systems.
|
|
6
|
+
* Each record carries the eight mandatory fields:
|
|
7
|
+
*
|
|
8
|
+
* 1. timestamp — ISO-8601 start time of the call
|
|
9
|
+
* 2. agentId — the calling agent (args.agentName) or the session id
|
|
10
|
+
* 3. toolName — the MCP tool that was invoked
|
|
11
|
+
* 4. inputParameters — tool arguments, with secret-bearing keys redacted
|
|
12
|
+
* 5. outputResult — outcome + byte size + a truncated, redacted preview
|
|
13
|
+
* 6. credentialsUsed — the session credential id (never the secret value)
|
|
14
|
+
* 7. userContext — requester/session context
|
|
15
|
+
* 8. outcomeStatus — success | error | unauthenticated
|
|
16
|
+
*
|
|
17
|
+
* Records are appended to `.mcp/audit/tool-calls.jsonl` (mode 0o600). For a
|
|
18
|
+
* tamper-proof deployment, point SECURITY_TOOL_AUDIT_LOG at a path backed by an
|
|
19
|
+
* append-only / write-once sink (e.g. an fs path on a volume with immutability,
|
|
20
|
+
* or a fifo forwarded to S3 Object Lock). Logging never throws: an audit-sink
|
|
21
|
+
* failure must not break tool execution.
|
|
22
|
+
*/
|
|
23
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { getSessionId, isAuthRequired } from "./auth.js";
|
|
26
|
+
const AUDIT_LOG_PATH = process.env.SECURITY_TOOL_AUDIT_LOG ?? join(".mcp", "audit", "tool-calls.jsonl");
|
|
27
|
+
const MAX_STRING_LEN = 512;
|
|
28
|
+
const MAX_ARRAY_LEN = 100;
|
|
29
|
+
const MAX_DEPTH = 6;
|
|
30
|
+
const MAX_OUTPUT_PREVIEW = 512;
|
|
31
|
+
const MAX_AGENT_ID_LEN = 256;
|
|
32
|
+
const MAX_AUDIT_BYTES = 50 * 1024 * 1024; // rotate the log once it exceeds 50 MB
|
|
33
|
+
// Keys whose values are credentials/secrets. Substring match (not anchored) so
|
|
34
|
+
// decorated variants are caught: sharedSecret, hmacKey, refreshToken, apiKeyHeader,
|
|
35
|
+
// clientSecretValue, SECURITY_MCP_SHARED_SECRET, x-api-key, etc.
|
|
36
|
+
const SENSITIVE_KEY_RE = /(?:secret|token|passw|pwd|api[_-]?key|apikey|authorization|auth|signature|hmac|private[_-]?key|access[_-]?key|bearer|cookie|credential)/i;
|
|
37
|
+
// Secret-shaped patterns scrubbed from string VALUES (and the output preview),
|
|
38
|
+
// regardless of key name — catches secrets embedded in URLs, command strings, and
|
|
39
|
+
// file contents returned by repo.read_file / repo.search.
|
|
40
|
+
const SECRET_VALUE_PATTERNS = [
|
|
41
|
+
/AKIA[0-9A-Z]{16}/g, // AWS access key id
|
|
42
|
+
/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/g, // PEM private key header
|
|
43
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, // JWT
|
|
44
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub token
|
|
45
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g, // Slack token
|
|
46
|
+
/(?:secret|token|password|passwd|api[_-]?key|access[_-]?key|private[_-]?key)["']?\s*[:=]\s*["']?[^\s"'`]{6,}/gi, // key=value
|
|
47
|
+
/\b[A-Fa-f0-9]{40,}\b/g, // long hex (keys/digests)
|
|
48
|
+
/\b[A-Za-z0-9+/]{40,}={0,2}\b/g // long base64 blob
|
|
49
|
+
];
|
|
50
|
+
function scrubSecrets(s) {
|
|
51
|
+
let out = s;
|
|
52
|
+
for (const re of SECRET_VALUE_PATTERNS)
|
|
53
|
+
out = out.replace(re, "[REDACTED]");
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Deep-clone arguments while masking secret keys and capping size. */
|
|
57
|
+
function redact(value, depth = 0) {
|
|
58
|
+
if (depth > MAX_DEPTH)
|
|
59
|
+
return "[depth-capped]";
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return value.slice(0, MAX_ARRAY_LEN).map((v) => redact(v, depth + 1));
|
|
62
|
+
}
|
|
63
|
+
if (value && typeof value === "object") {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [k, v] of Object.entries(value)) {
|
|
66
|
+
out[k] = SENSITIVE_KEY_RE.test(k) ? "[REDACTED]" : redact(v, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
const scrubbed = scrubSecrets(value);
|
|
72
|
+
return scrubbed.length > MAX_STRING_LEN ? scrubbed.slice(0, MAX_STRING_LEN) + "…[truncated]" : scrubbed;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
/** Classify a tool result (the asTextResponse shape) into an outcome status. */
|
|
77
|
+
export function classifyOutcome(result) {
|
|
78
|
+
try {
|
|
79
|
+
const text = result?.content?.[0]?.text;
|
|
80
|
+
if (typeof text === "string") {
|
|
81
|
+
if (text.startsWith("[security-mcp error]"))
|
|
82
|
+
return "error";
|
|
83
|
+
// Match the structured framings only — not the bare word, which could appear in
|
|
84
|
+
// returned file content (repo.read_file) and poison the outcome field.
|
|
85
|
+
if (/"error"\s*:\s*"UNAUTHENTICATED"/.test(text))
|
|
86
|
+
return "unauthenticated";
|
|
87
|
+
if (/"authenticated"\s*:\s*false/.test(text))
|
|
88
|
+
return "unauthenticated"; // failed auth attempt
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* fall through to success */
|
|
93
|
+
}
|
|
94
|
+
return "success";
|
|
95
|
+
}
|
|
96
|
+
function summarizeOutput(result, outcome) {
|
|
97
|
+
let preview = "";
|
|
98
|
+
let bytes = 0;
|
|
99
|
+
try {
|
|
100
|
+
const text = result?.content?.[0]?.text;
|
|
101
|
+
if (typeof text === "string") {
|
|
102
|
+
bytes = Buffer.byteLength(text, "utf-8");
|
|
103
|
+
// Scrub secrets/PII before previewing — tool outputs include repo file contents.
|
|
104
|
+
const scrubbed = scrubSecrets(text);
|
|
105
|
+
preview = scrubbed.length > MAX_OUTPUT_PREVIEW ? scrubbed.slice(0, MAX_OUTPUT_PREVIEW) + "…[truncated]" : scrubbed;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* leave defaults */
|
|
110
|
+
}
|
|
111
|
+
return { outcome, bytes, preview };
|
|
112
|
+
}
|
|
113
|
+
function extractAgentId(args) {
|
|
114
|
+
if (args && typeof args === "object" && "agentName" in args) {
|
|
115
|
+
const a = args.agentName;
|
|
116
|
+
if (typeof a === "string" && a.length > 0)
|
|
117
|
+
return a.slice(0, MAX_AGENT_ID_LEN);
|
|
118
|
+
}
|
|
119
|
+
return (getSessionId() ?? "mcp-session").slice(0, MAX_AGENT_ID_LEN);
|
|
120
|
+
}
|
|
121
|
+
function safeStringify(entry) {
|
|
122
|
+
// Coerce BigInt so JSON.stringify never throws — a throw would silently drop the
|
|
123
|
+
// record, which an attacker could weaponize as an audit-evasion primitive.
|
|
124
|
+
return JSON.stringify(entry, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
125
|
+
}
|
|
126
|
+
/** Append one audit record. Swallows all errors — never breaks tool execution. */
|
|
127
|
+
function recordToolCall(entry) {
|
|
128
|
+
try {
|
|
129
|
+
mkdirSync(dirname(AUDIT_LOG_PATH), { recursive: true, mode: 0o700 });
|
|
130
|
+
// CWE-400: single-rotation size guard so a tight tool-call loop cannot exhaust disk.
|
|
131
|
+
try {
|
|
132
|
+
if (statSync(AUDIT_LOG_PATH).size > MAX_AUDIT_BYTES) {
|
|
133
|
+
renameSync(AUDIT_LOG_PATH, `${AUDIT_LOG_PATH}.1`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* file absent or not rotatable — ignore */
|
|
138
|
+
}
|
|
139
|
+
let line;
|
|
140
|
+
try {
|
|
141
|
+
line = safeStringify(entry);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Last-resort minimal record so a sensitive call is never invisible in the log.
|
|
145
|
+
line = JSON.stringify({
|
|
146
|
+
timestamp: entry.timestamp,
|
|
147
|
+
toolName: entry.toolName,
|
|
148
|
+
outcomeStatus: entry.outcomeStatus,
|
|
149
|
+
note: "serialize-failed"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
appendFileSync(AUDIT_LOG_PATH, line + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* audit sink unavailable — do not interrupt the tool call */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Wrap an MCP tool handler so every invocation emits one structured audit
|
|
160
|
+
* record. The handler's behaviour and return value are unchanged.
|
|
161
|
+
*/
|
|
162
|
+
export function withToolAudit(toolName, handler) {
|
|
163
|
+
const wrapped = async (args, extra) => {
|
|
164
|
+
const startedAt = new Date().toISOString();
|
|
165
|
+
const start = Date.now();
|
|
166
|
+
let result;
|
|
167
|
+
let outcome = "success";
|
|
168
|
+
try {
|
|
169
|
+
result = await handler(args, extra);
|
|
170
|
+
outcome = classifyOutcome(result);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
outcome = "error";
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
const sessionId = getSessionId();
|
|
179
|
+
recordToolCall({
|
|
180
|
+
timestamp: startedAt,
|
|
181
|
+
durationMs: Date.now() - start,
|
|
182
|
+
agentId: extractAgentId(args),
|
|
183
|
+
toolName,
|
|
184
|
+
inputParameters: redact(args),
|
|
185
|
+
outputResult: summarizeOutput(result, outcome),
|
|
186
|
+
credentialsUsed: sessionId ?? (isAuthRequired() ? "unauthenticated" : "no-auth-configured"),
|
|
187
|
+
userContext: `session:${sessionId ?? "anonymous"} pid:${process.pid}`,
|
|
188
|
+
outcomeStatus: outcome
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
return wrapped;
|
|
193
|
+
}
|
package/dist/repo/fs.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
// Upper bound on the size of any single file the gate will read into memory.
|
|
4
|
+
// A malicious target repo can otherwise ship multi-GB files (or one huge
|
|
5
|
+
// contiguous token) to exhaust memory, or trigger V8 RangeError in the
|
|
6
|
+
// secret-scanner's global-regex passes. 10 MB comfortably covers real source,
|
|
7
|
+
// lockfiles, and minified bundles while bounding blast radius. CWE-400 / CWE-789.
|
|
8
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
3
9
|
function getWorkspaceRoot() {
|
|
4
10
|
return process.cwd();
|
|
5
11
|
}
|
|
@@ -16,5 +22,35 @@ export async function readFileSafe(relPath) {
|
|
|
16
22
|
if (p !== root && !p.startsWith(rootPrefix)) {
|
|
17
23
|
throw new Error("Path traversal blocked");
|
|
18
24
|
}
|
|
25
|
+
// Resolve symlinks and verify the real path is also within the workspace.
|
|
26
|
+
// This prevents symlink traversal attacks where a symlink inside the workspace
|
|
27
|
+
// points to a file outside it. CWE-61 / CAPEC-132.
|
|
28
|
+
try {
|
|
29
|
+
const realResolved = await realpath(p);
|
|
30
|
+
const realRoot = await realpath(root);
|
|
31
|
+
const realRootPrefix = realRoot + path.sep;
|
|
32
|
+
if (realResolved !== realRoot && !realResolved.startsWith(realRootPrefix)) {
|
|
33
|
+
throw new Error(`Symlink traversal detected: ${relPath} -> ${realResolved}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (e.code === "ENOENT") {
|
|
38
|
+
throw new Error(`File not found: ${relPath}`);
|
|
39
|
+
}
|
|
40
|
+
if (e.message.includes("Symlink traversal"))
|
|
41
|
+
throw e;
|
|
42
|
+
// SECURITY: Any other realpath error (EACCES, ELOOP, etc.) means we could not
|
|
43
|
+
// verify the real path is within the workspace. Deny rather than fall through,
|
|
44
|
+
// because readFile() would follow symlinks using the unverified lexical path,
|
|
45
|
+
// enabling traversal to out-of-workspace targets. CWE-61 / CAPEC-132.
|
|
46
|
+
throw new Error(`Cannot verify path safety for ${relPath}: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
// CWE-400/CWE-789: refuse oversized files so a hostile repo cannot exhaust
|
|
49
|
+
// memory or feed a multi-MB contiguous token into a global regex (RangeError).
|
|
50
|
+
// Loop-callers (secret/cloud-controls/search scanners) catch this and skip the file.
|
|
51
|
+
const { size } = await stat(p);
|
|
52
|
+
if (size > MAX_FILE_BYTES) {
|
|
53
|
+
throw new Error(`File too large to scan safely: ${relPath} (${size} bytes > ${MAX_FILE_BYTES})`);
|
|
54
|
+
}
|
|
19
55
|
return await readFile(p, "utf8");
|
|
20
56
|
}
|
package/dist/repo/search.js
CHANGED
|
@@ -2,10 +2,30 @@ import fg from "fast-glob";
|
|
|
2
2
|
import { readFileSafe } from "./fs.js";
|
|
3
3
|
// Maximum allowed regex pattern length. Longer patterns significantly raise
|
|
4
4
|
// the risk of catastrophic backtracking (ReDoS). CWE-1333.
|
|
5
|
-
const MAX_REGEX_LEN =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const MAX_REGEX_LEN = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Detects regex patterns that risk catastrophic backtracking (ReDoS).
|
|
8
|
+
* Covers nested quantifiers, ambiguous alternation with outer quantifiers,
|
|
9
|
+
* counted repetition inside groups, and overlapping wildcard groups.
|
|
10
|
+
* CWE-1333 / MITRE ATT&CK T1499.
|
|
11
|
+
*/
|
|
12
|
+
function isCatastrophicRegex(pattern) {
|
|
13
|
+
// Original: nested quantifiers like (a+)+, (a*)*, (\w+)+
|
|
14
|
+
if (/\([^)]*[+*][^)]*\)[+*?{]/.test(pattern))
|
|
15
|
+
return true;
|
|
16
|
+
// Ambiguous alternation with outer quantifier: (a|aa)+ or (a|b)+
|
|
17
|
+
if (/\([^)]*\|[^)]*\)[+*]/.test(pattern))
|
|
18
|
+
return true;
|
|
19
|
+
// Counted repetition with nested group: (a{2,})+ or (a{1,3})+
|
|
20
|
+
if (/\([^)]*\{[^)]*\}[^)]*\)[+*]/.test(pattern))
|
|
21
|
+
return true;
|
|
22
|
+
// Overlapping alternatives: (.+)+ or (\w+)+
|
|
23
|
+
if (/\(\.[+*][^)]*\)[+*]/.test(pattern))
|
|
24
|
+
return true;
|
|
25
|
+
if (/\(\\[wWdDsS][+*][^)]*\)[+*]/.test(pattern))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
9
29
|
/**
|
|
10
30
|
* Validates and compiles a user-supplied regex string.
|
|
11
31
|
* Throws if the pattern is dangerously long, contains known ReDoS signatures,
|
|
@@ -16,12 +36,16 @@ function compileUserRegex(pattern) {
|
|
|
16
36
|
if (pattern.length > MAX_REGEX_LEN) {
|
|
17
37
|
throw new Error(`Regex pattern too long (max ${MAX_REGEX_LEN} chars)`);
|
|
18
38
|
}
|
|
19
|
-
if (
|
|
39
|
+
if (isCatastrophicRegex(pattern)) {
|
|
20
40
|
throw new Error("Regex pattern contains nested quantifiers that risk catastrophic backtracking (ReDoS)");
|
|
21
41
|
}
|
|
22
42
|
return new RegExp(pattern, "i"); // throws SyntaxError on invalid patterns
|
|
23
43
|
}
|
|
24
44
|
const MAX_PREVIEW_LEN = 240;
|
|
45
|
+
const SECRET_REDACT_RE = /\b(?:AKIA[A-Z0-9]{16}|sk-[A-Za-z0-9]{32,}|ghp_[A-Za-z0-9]{36,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{20,}){2})\b/g;
|
|
46
|
+
function redactSecrets(s) {
|
|
47
|
+
return s.replace(SECRET_REDACT_RE, "[REDACTED]");
|
|
48
|
+
}
|
|
25
49
|
function isHit(line, query, re) {
|
|
26
50
|
return re ? re.test(line) : line.includes(query);
|
|
27
51
|
}
|
|
@@ -35,13 +59,14 @@ function scanLines(file, lines, opts, re, matches) {
|
|
|
35
59
|
matches.push({
|
|
36
60
|
file,
|
|
37
61
|
line: i + 1,
|
|
38
|
-
preview: line.slice(0, MAX_PREVIEW_LEN)
|
|
62
|
+
preview: redactSecrets(line.slice(0, MAX_PREVIEW_LEN))
|
|
39
63
|
});
|
|
40
64
|
}
|
|
41
65
|
}
|
|
42
66
|
export async function searchRepo(opts) {
|
|
43
67
|
const files = await fg(["**/*.*"], {
|
|
44
68
|
dot: true,
|
|
69
|
+
followSymbolicLinks: false, // Prevent glob-based symlink traversal outside workspace root.
|
|
45
70
|
ignore: [
|
|
46
71
|
"**/node_modules/**",
|
|
47
72
|
"**/.git/**",
|
package/dist/review/store.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const REVIEW_DIR = path.join(".mcp", "reviews");
|
|
5
5
|
const REPORT_DIR = path.join(".mcp", "reports");
|
|
6
6
|
const CHECKLIST_DEFAULTS_DIR = path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "defaults", "checklists");
|
|
7
7
|
async function ensureDir(dirPath) {
|
|
8
|
-
await mkdir(dirPath, { recursive: true });
|
|
8
|
+
await mkdir(dirPath, { recursive: true, mode: 0o700 });
|
|
9
9
|
}
|
|
10
10
|
function reviewPath(runId) {
|
|
11
11
|
return path.join(process.cwd(), REVIEW_DIR, `${runId}.json`);
|
|
@@ -15,7 +15,7 @@ function reportPath(runId) {
|
|
|
15
15
|
}
|
|
16
16
|
async function writeJson(filePath, value) {
|
|
17
17
|
await ensureDir(path.dirname(filePath));
|
|
18
|
-
await writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
18
|
+
await writeFile(filePath, JSON.stringify(value, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
19
19
|
}
|
|
20
20
|
function checklistPath(runId) {
|
|
21
21
|
return path.join(process.cwd(), REVIEW_DIR, `${runId}-checklist.json`);
|
|
@@ -40,6 +40,7 @@ const SAFE_SURFACE_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
|
40
40
|
* Initialize a checklist for a run from the surface template.
|
|
41
41
|
*/
|
|
42
42
|
export async function initChecklist(runId, surface) {
|
|
43
|
+
assertRunId(runId); // CWE-22: validate UUID format before using as filename component
|
|
43
44
|
if (!SAFE_SURFACE_RE.test(surface)) {
|
|
44
45
|
throw new Error(`Invalid surface name "${surface}"`);
|
|
45
46
|
}
|
|
@@ -74,6 +75,7 @@ export async function initChecklist(runId, surface) {
|
|
|
74
75
|
* Mark a checklist item as completed.
|
|
75
76
|
*/
|
|
76
77
|
export async function completeChecklistItem(runId, itemId, completedBy, evidence) {
|
|
78
|
+
assertRunId(runId); // CWE-22
|
|
77
79
|
const state = await readChecklistRaw(runId);
|
|
78
80
|
if (!state)
|
|
79
81
|
throw new Error(`No checklist found for runId: ${runId}`);
|
|
@@ -93,6 +95,7 @@ export async function completeChecklistItem(runId, itemId, completedBy, evidence
|
|
|
93
95
|
* Mark a checklist item as not applicable.
|
|
94
96
|
*/
|
|
95
97
|
export async function markChecklistItemNA(runId, itemId, completedBy, reason) {
|
|
98
|
+
assertRunId(runId); // CWE-22
|
|
96
99
|
const state = await readChecklistRaw(runId);
|
|
97
100
|
if (!state)
|
|
98
101
|
throw new Error(`No checklist found for runId: ${runId}`);
|
|
@@ -111,6 +114,7 @@ export async function markChecklistItemNA(runId, itemId, completedBy, reason) {
|
|
|
111
114
|
* Mark a checklist item as failed.
|
|
112
115
|
*/
|
|
113
116
|
export async function failChecklistItem(runId, itemId, completedBy, reason) {
|
|
117
|
+
assertRunId(runId); // CWE-22
|
|
114
118
|
const state = await readChecklistRaw(runId);
|
|
115
119
|
if (!state)
|
|
116
120
|
throw new Error(`No checklist found for runId: ${runId}`);
|
|
@@ -129,6 +133,7 @@ export async function failChecklistItem(runId, itemId, completedBy, reason) {
|
|
|
129
133
|
* Sign off on a checklist. Requires all non-NA critical items to be completed.
|
|
130
134
|
*/
|
|
131
135
|
export async function signOffChecklist(runId, signedOffBy) {
|
|
136
|
+
assertRunId(runId); // CWE-22
|
|
132
137
|
const state = await readChecklistRaw(runId);
|
|
133
138
|
if (!state)
|
|
134
139
|
throw new Error(`No checklist found for runId: ${runId}`);
|
|
@@ -147,6 +152,7 @@ export async function signOffChecklist(runId, signedOffBy) {
|
|
|
147
152
|
* Read checklist state for a run.
|
|
148
153
|
*/
|
|
149
154
|
export async function readChecklist(runId) {
|
|
155
|
+
assertRunId(runId); // CWE-22
|
|
150
156
|
return readChecklistRaw(runId);
|
|
151
157
|
}
|
|
152
158
|
export async function createReviewRun(opts) {
|
|
@@ -157,6 +163,7 @@ export async function createReviewRun(opts) {
|
|
|
157
163
|
createdAt: now,
|
|
158
164
|
updatedAt: now,
|
|
159
165
|
mode: opts.mode,
|
|
166
|
+
remediationMode: opts.remediationMode,
|
|
160
167
|
targets: cleanTargets,
|
|
161
168
|
baseRef: opts.baseRef,
|
|
162
169
|
headRef: opts.headRef,
|
|
@@ -167,6 +174,7 @@ export async function createReviewRun(opts) {
|
|
|
167
174
|
updatedAt: now,
|
|
168
175
|
details: {
|
|
169
176
|
mode: opts.mode,
|
|
177
|
+
remediationMode: opts.remediationMode,
|
|
170
178
|
targets: cleanTargets,
|
|
171
179
|
baseRef: opts.baseRef,
|
|
172
180
|
headRef: opts.headRef
|
|
@@ -202,7 +210,16 @@ export async function updateReviewStep(runId, step, status, details) {
|
|
|
202
210
|
await writeJson(reviewPath(run.id), run);
|
|
203
211
|
return run;
|
|
204
212
|
}
|
|
213
|
+
// HMAC-SHA256 requires a key of at least 32 bytes (256 bits) to provide full
|
|
214
|
+
// security. Keys shorter than the hash output degrade HMAC to effectively a
|
|
215
|
+
// keyed hash with reduced security margin (NIST SP 800-107 §5.3.4).
|
|
216
|
+
const HMAC_MIN_KEY_BYTES = 32;
|
|
205
217
|
export async function createReviewAttestation(runId, payload, signatureKey) {
|
|
218
|
+
if (signatureKey !== undefined && Buffer.byteLength(signatureKey, "utf-8") < HMAC_MIN_KEY_BYTES) {
|
|
219
|
+
throw new Error(`HMAC signature key is too short (${Buffer.byteLength(signatureKey, "utf-8")} bytes). ` +
|
|
220
|
+
`Provide a key of at least ${HMAC_MIN_KEY_BYTES} bytes (256 bits) — ` +
|
|
221
|
+
`generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
|
|
222
|
+
}
|
|
206
223
|
const digestInput = JSON.stringify(payload);
|
|
207
224
|
const sha256 = createHash("sha256").update(digestInput).digest("hex");
|
|
208
225
|
const hmacSha256 = signatureKey
|
|
@@ -221,3 +238,39 @@ export async function createReviewAttestation(runId, payload, signatureKey) {
|
|
|
221
238
|
hmacSha256
|
|
222
239
|
};
|
|
223
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Verify a stored attestation HMAC using a timing-safe comparison.
|
|
243
|
+
* Returns true only if the stored hmacSha256 matches the recomputed value.
|
|
244
|
+
* Uses timingSafeEqual to prevent timing oracle attacks on the comparison.
|
|
245
|
+
*/
|
|
246
|
+
export async function verifyAttestationHmac(runId, signatureKey) {
|
|
247
|
+
if (Buffer.byteLength(signatureKey, "utf-8") < HMAC_MIN_KEY_BYTES) {
|
|
248
|
+
return { valid: false, reason: "Signature key too short — cannot verify." };
|
|
249
|
+
}
|
|
250
|
+
let stored;
|
|
251
|
+
try {
|
|
252
|
+
const raw = await readFile(reportPath(runId), "utf-8");
|
|
253
|
+
stored = JSON.parse(raw);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return { valid: false, reason: "Attestation file not found or unreadable." };
|
|
257
|
+
}
|
|
258
|
+
const integrity = stored["integrity"];
|
|
259
|
+
const storedHmac = typeof integrity?.["hmacSha256"] === "string" ? integrity["hmacSha256"] : null;
|
|
260
|
+
if (!storedHmac) {
|
|
261
|
+
return { valid: false, reason: "Attestation was not signed — no hmacSha256 field." };
|
|
262
|
+
}
|
|
263
|
+
// Recompute HMAC over payload (everything except the integrity wrapper)
|
|
264
|
+
const { integrity: _stripped, ...payloadOnly } = stored;
|
|
265
|
+
const digestInput = JSON.stringify(payloadOnly);
|
|
266
|
+
const expected = createHmac("sha256", signatureKey).update(digestInput).digest("hex");
|
|
267
|
+
// Timing-safe comparison — prevents oracle attacks that leak the correct HMAC
|
|
268
|
+
// byte-by-byte via response timing differences (CWE-208).
|
|
269
|
+
const storedBuf = Buffer.from(storedHmac, "hex");
|
|
270
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
271
|
+
if (storedBuf.length !== expectedBuf.length) {
|
|
272
|
+
return { valid: false, reason: "HMAC length mismatch." };
|
|
273
|
+
}
|
|
274
|
+
const match = timingSafeEqual(storedBuf, expectedBuf);
|
|
275
|
+
return match ? { valid: true } : { valid: false, reason: "HMAC mismatch — attestation may have been tampered." };
|
|
276
|
+
}
|
package/dist/tests/run.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { runPrGate } from "../gate/policy.js";
|
|
6
|
+
import { autoHardenTree } from "../gate/cloud-controls/apply.js";
|
|
7
|
+
import { checkCloudControls } from "../gate/checks/cloud-controls.js";
|
|
5
8
|
import { createReviewAttestation, createReviewRun, readReviewRun, updateReviewStep } from "../review/store.js";
|
|
6
9
|
function repoPath(...parts) {
|
|
7
10
|
return path.join(process.cwd(), ...parts);
|
|
@@ -67,13 +70,115 @@ async function runFixtureGateTests() {
|
|
|
67
70
|
});
|
|
68
71
|
const ids = result.findings.map((finding) => finding.id);
|
|
69
72
|
assert.ok(ids.includes("AI_OUTPUT_BOUNDS_MISSING"));
|
|
73
|
+
assert.ok(ids.includes("AI_BIAS_TESTING_ABSENT"));
|
|
74
|
+
});
|
|
75
|
+
await withFixture("agentic-malicious", async () => {
|
|
76
|
+
const result = await runPrGate({
|
|
77
|
+
mode: "folder_by_folder",
|
|
78
|
+
targets: ["."],
|
|
79
|
+
policyPath: ".mcp/policies/security-policy.json"
|
|
80
|
+
});
|
|
81
|
+
const ids = result.findings.map((finding) => finding.id);
|
|
82
|
+
assert.ok(ids.includes("AGENT_INSTRUCTION_OVERRIDE"));
|
|
83
|
+
assert.ok(ids.includes("AGENT_INSTRUCTION_EXFIL"));
|
|
84
|
+
assert.ok(ids.includes("AGENT_PERSISTENCE_DIRECTIVE"));
|
|
85
|
+
assert.ok(ids.includes("AGENT_TOOL_POISONING"));
|
|
86
|
+
assert.ok(ids.includes("AGENT_CREDENTIAL_HARVEST"));
|
|
87
|
+
assert.ok(ids.includes("AGENT_MEMORY_POISONING"));
|
|
88
|
+
assert.ok(ids.includes("AGENT_HIDDEN_INSTRUCTION"));
|
|
89
|
+
assert.ok(ids.includes("AGENT_REMOTE_INSTRUCTION_LOAD"));
|
|
90
|
+
assert.ok(ids.includes("AGENT_PERMISSION_ESCALATION"));
|
|
91
|
+
assert.ok(ids.includes("AGENT_BACKDOOR_INSERT"));
|
|
92
|
+
assert.ok(ids.includes("AGENT_PROMPT_LEAK"));
|
|
93
|
+
});
|
|
94
|
+
await withFixture("aws-insecure", async () => {
|
|
95
|
+
const result = await runPrGate({
|
|
96
|
+
mode: "folder_by_folder",
|
|
97
|
+
targets: ["terraform"],
|
|
98
|
+
policyPath: ".mcp/policies/security-policy.json"
|
|
99
|
+
});
|
|
100
|
+
const ids = result.findings.map((finding) => finding.id);
|
|
101
|
+
assert.ok(ids.includes("AWS_EC2_IMDSV2_REQUIRED"));
|
|
102
|
+
assert.ok(ids.includes("AWS_RDS_NOT_PUBLIC"));
|
|
103
|
+
assert.ok(ids.includes("AWS_S3_BUCKET_NO_PUBLIC_ACL"));
|
|
104
|
+
assert.ok(ids.includes("AWS_S3_BLOCK_PUBLIC_ACCESS"));
|
|
105
|
+
assert.ok(ids.includes("AWS_LAMBDA_URL_AUTH_REQUIRED"));
|
|
70
106
|
});
|
|
71
107
|
}
|
|
108
|
+
async function runCloudControlRemediationTests() {
|
|
109
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "aws-harden-"));
|
|
110
|
+
const previous = process.cwd();
|
|
111
|
+
try {
|
|
112
|
+
cpSync(repoPath("fixtures", "aws-insecure", "terraform"), path.join(tmp, "terraform"), {
|
|
113
|
+
recursive: true
|
|
114
|
+
});
|
|
115
|
+
process.chdir(tmp);
|
|
116
|
+
const first = await autoHardenTree({ write: true });
|
|
117
|
+
const appliedIds = new Set(first.applied.map((fix) => fix.ruleId));
|
|
118
|
+
assert.ok(appliedIds.has("AWS_EC2_IMDSV2_REQUIRED"));
|
|
119
|
+
assert.ok(appliedIds.has("AWS_RDS_NOT_PUBLIC"));
|
|
120
|
+
assert.ok(appliedIds.has("AWS_S3_BUCKET_NO_PUBLIC_ACL"));
|
|
121
|
+
assert.ok(appliedIds.has("AWS_S3_BLOCK_PUBLIC_ACCESS"));
|
|
122
|
+
assert.ok(appliedIds.has("AWS_KMS_KEY_ROTATION"));
|
|
123
|
+
assert.ok(appliedIds.has("AWS_LAMBDA_URL_AUTH_REQUIRED"));
|
|
124
|
+
const hardened = readFileSync(path.join(tmp, "terraform", "main.tf"), "utf-8");
|
|
125
|
+
assert.match(hardened, /http_tokens\s*=\s*"required"/);
|
|
126
|
+
assert.match(hardened, /publicly_accessible\s*=\s*false/);
|
|
127
|
+
assert.match(hardened, /acl\s*=\s*"private"/);
|
|
128
|
+
assert.match(hardened, /enable_key_rotation\s*=\s*true/);
|
|
129
|
+
assert.match(hardened, /authorization_type\s*=\s*"AWS_IAM"/);
|
|
130
|
+
assert.match(hardened, /aws_s3_bucket_public_access_block/);
|
|
131
|
+
// Idempotent: a second pass over the now-hardened tree applies nothing.
|
|
132
|
+
const second = await autoHardenTree({ write: true });
|
|
133
|
+
assert.equal(second.applied.length, 0);
|
|
134
|
+
assert.equal(second.filesChanged.length, 0);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
process.chdir(previous);
|
|
138
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function runNestedRemediationTests() {
|
|
142
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "cloud-harden-"));
|
|
143
|
+
const previous = process.cwd();
|
|
144
|
+
try {
|
|
145
|
+
cpSync(repoPath("fixtures", "gcp-insecure", "terraform"), path.join(tmp, "gcp"), {
|
|
146
|
+
recursive: true
|
|
147
|
+
});
|
|
148
|
+
cpSync(repoPath("fixtures", "azure-insecure", "terraform"), path.join(tmp, "azure"), {
|
|
149
|
+
recursive: true
|
|
150
|
+
});
|
|
151
|
+
process.chdir(tmp);
|
|
152
|
+
const report = await autoHardenTree({ write: true });
|
|
153
|
+
const appliedIds = new Set(report.applied.map((fix) => fix.ruleId));
|
|
154
|
+
// GCP: depth-3 nested replace + insert into existing settings/ip_configuration blocks.
|
|
155
|
+
assert.ok(appliedIds.has("GCP_SQL_NO_PUBLIC_IP"));
|
|
156
|
+
assert.ok(appliedIds.has("GCP_SQL_REQUIRE_SSL"));
|
|
157
|
+
assert.ok(appliedIds.has("GCP_STORAGE_UNIFORM_ACCESS"));
|
|
158
|
+
// Azure.
|
|
159
|
+
assert.ok(appliedIds.has("AZURE_STORAGE_HTTPS_ONLY"));
|
|
160
|
+
assert.ok(appliedIds.has("AZURE_KV_PURGE_PROTECTION"));
|
|
161
|
+
const gcp = readFileSync(path.join(tmp, "gcp", "main.tf"), "utf-8");
|
|
162
|
+
assert.match(gcp, /ipv4_enabled\s*=\s*false/);
|
|
163
|
+
assert.match(gcp, /require_ssl\s*=\s*true/);
|
|
164
|
+
const azure = readFileSync(path.join(tmp, "azure", "main.tf"), "utf-8");
|
|
165
|
+
assert.match(azure, /enable_https_traffic_only\s*=\s*true/);
|
|
166
|
+
assert.match(azure, /purge_protection_enabled\s*=\s*true/);
|
|
167
|
+
// Idempotent across both providers.
|
|
168
|
+
const second = await autoHardenTree({ write: true });
|
|
169
|
+
assert.equal(second.applied.length, 0);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
process.chdir(previous);
|
|
173
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
72
176
|
async function runReviewWorkflowTests() {
|
|
73
177
|
cleanupFixtureReviewArtifacts("web-insecure");
|
|
74
178
|
await withFixture("web-insecure", async () => {
|
|
75
179
|
const run = await createReviewRun({
|
|
76
180
|
mode: "folder_by_folder",
|
|
181
|
+
remediationMode: "auto_apply",
|
|
77
182
|
targets: ["src"]
|
|
78
183
|
});
|
|
79
184
|
await updateReviewStep(run.id, "scan_strategy", "completed", { mode: "folder_by_folder", targets: ["src"] });
|
|
@@ -91,9 +196,27 @@ async function runReviewWorkflowTests() {
|
|
|
91
196
|
});
|
|
92
197
|
cleanupFixtureReviewArtifacts("web-insecure");
|
|
93
198
|
}
|
|
199
|
+
async function runCfnBicepDetectionTests() {
|
|
200
|
+
await withFixture("cfn-insecure", async () => {
|
|
201
|
+
const ids = new Set((await checkCloudControls({ changedFiles: [] })).map((f) => f.id));
|
|
202
|
+
assert.ok(ids.has("CFN_S3_NO_PUBLIC_ACL"));
|
|
203
|
+
assert.ok(ids.has("CFN_RDS_NOT_PUBLIC"));
|
|
204
|
+
assert.ok(ids.has("CFN_RDS_STORAGE_ENCRYPTED"));
|
|
205
|
+
assert.ok(ids.has("CFN_SG_OPEN_INGRESS"));
|
|
206
|
+
});
|
|
207
|
+
await withFixture("bicep-insecure", async () => {
|
|
208
|
+
const ids = new Set((await checkCloudControls({ changedFiles: [] })).map((f) => f.id));
|
|
209
|
+
assert.ok(ids.has("BICEP_STORAGE_HTTPS_ONLY"));
|
|
210
|
+
assert.ok(ids.has("BICEP_STORAGE_MIN_TLS"));
|
|
211
|
+
assert.ok(ids.has("BICEP_SQL_NO_PUBLIC"));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
94
214
|
async function main() {
|
|
95
215
|
await runPromptConformanceTests();
|
|
96
216
|
await runFixtureGateTests();
|
|
217
|
+
await runCloudControlRemediationTests();
|
|
218
|
+
await runNestedRemediationTests();
|
|
219
|
+
await runCfnBicepDetectionTests();
|
|
97
220
|
await runReviewWorkflowTests();
|
|
98
221
|
console.log("security-mcp tests passed");
|
|
99
222
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "security-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "AI security MCP server and enforcement gate for Claude Code, Cursor, GitHub Copilot, Codex, Replit, and any MCP-compatible editor. Applies OWASP, MITRE ATT&CK, NIST, Zero Trust, PCI DSS, SOC 2, and ISO 27001.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"homepage": "https://github.com/AbrahamOO/security-mcp#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/AbrahamOO/security-mcp.git"
|
|
11
|
+
"url": "git+https://github.com/AbrahamOO/security-mcp.git"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/AbrahamOO/security-mcp/issues"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"model-context-protocol"
|
|
42
42
|
],
|
|
43
43
|
"bin": {
|
|
44
|
-
"security-mcp": "
|
|
44
|
+
"security-mcp": "dist/cli/index.js"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"dist/",
|
|
@@ -61,24 +61,24 @@
|
|
|
61
61
|
"test": "npm run build && node dist/tests/run.js"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
65
65
|
"execa": "^9.5.2",
|
|
66
66
|
"fast-glob": "^3.3.3",
|
|
67
67
|
"picomatch": "^4.0.4",
|
|
68
68
|
"zod": "^3.24.1"
|
|
69
69
|
},
|
|
70
70
|
"overrides": {
|
|
71
|
-
"express-rate-limit": "
|
|
72
|
-
"hono": "
|
|
71
|
+
"express-rate-limit": "8.5.2",
|
|
72
|
+
"hono": "4.12.23"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@eslint/js": "^9.22.0",
|
|
76
|
-
"@types/node": "^
|
|
77
|
-
"@types/picomatch": "^4.0.
|
|
76
|
+
"@types/node": "^24.12.4",
|
|
77
|
+
"@types/picomatch": "^4.0.3",
|
|
78
78
|
"eslint": "^9.22.0",
|
|
79
79
|
"globals": "^16.0.0",
|
|
80
80
|
"typescript": "^5.7.3",
|
|
81
|
-
"typescript-eslint": "^8.
|
|
81
|
+
"typescript-eslint": "^8.60.0"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|
|
84
84
|
"node": ">=20"
|