security-mcp 1.1.3 → 1.3.1
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 +164 -185
- 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/control-catalog.json +200 -0
- package/defaults/security-policy.json +2 -2
- package/dist/cli/index.js +82 -5
- package/dist/cli/install.js +36 -6
- package/dist/cli/onboarding.js +6 -0
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +935 -0
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/dependencies.js +571 -15
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +848 -0
- package/dist/gate/checks/k8s.js +114 -1
- 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 +244 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +131 -9
- package/dist/gate/policy.js +282 -129
- package/dist/mcp/audit-chain.js +122 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +158 -21
- package/dist/mcp/orchestration.js +186 -51
- package/dist/mcp/server.js +608 -94
- package/dist/repo/fs.js +24 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +52 -1
- package/package.json +7 -7
- package/prompts/SECURITY_PROMPT.md +73 -0
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +109 -0
- package/skills/agentic-loop-exploiter/SKILL.md +368 -0
- package/skills/ai-llm-redteam/SKILL.md +104 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
- package/skills/android-penetration-tester/SKILL.md +455 -46
- package/skills/anti-replay-tester/SKILL.md +106 -0
- package/skills/appsec-code-auditor/SKILL.md +120 -0
- package/skills/artifact-integrity-analyst/SKILL.md +441 -0
- package/skills/attack-navigator/SKILL.md +467 -8
- package/skills/auth-session-hacker/SKILL.md +128 -0
- package/skills/aws-penetration-tester/SKILL.md +456 -0
- package/skills/azure-penetration-tester/SKILL.md +490 -3
- package/skills/binary-auth-validator/SKILL.md +111 -0
- package/skills/bot-detection-specialist/SKILL.md +109 -0
- package/skills/business-logic-attacker/SKILL.md +231 -0
- package/skills/capec-code-mapper/SKILL.md +84 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
- package/skills/ciso-orchestrator/SKILL.md +454 -43
- package/skills/cloud-infra-specialist/SKILL.md +118 -0
- package/skills/compliance-gap-analyst/SKILL.md +422 -0
- package/skills/compliance-grc/SKILL.md +85 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
- package/skills/credential-stuffing-specialist/SKILL.md +102 -0
- package/skills/crypto-pki-specialist/SKILL.md +87 -0
- package/skills/csa-ccm-mapper/SKILL.md +84 -0
- package/skills/csf2-governance-mapper/SKILL.md +84 -0
- package/skills/deep-link-fuzzer/SKILL.md +109 -0
- package/skills/dependency-confusion-attacker/SKILL.md +415 -0
- package/skills/device-integrity-aggregator/SKILL.md +108 -0
- package/skills/dos-resilience-tester/SKILL.md +97 -0
- package/skills/dread-scorer/SKILL.md +84 -0
- package/skills/egress-policy-enforcer/SKILL.md +99 -0
- package/skills/evidence-collector/SKILL.md +98 -0
- package/skills/file-upload-attacker/SKILL.md +109 -0
- package/skills/gcp-penetration-tester/SKILL.md +459 -2
- package/skills/git-history-secret-scanner/SKILL.md +106 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
- package/skills/incident-responder/SKILL.md +111 -0
- package/skills/injection-specialist/SKILL.md +131 -0
- package/skills/ios-security-auditor/SKILL.md +282 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +384 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
- package/skills/kill-switch-engineer/SKILL.md +102 -0
- package/skills/linddun-privacy-analyst/SKILL.md +102 -0
- package/skills/logic-race-fuzzer/SKILL.md +443 -0
- package/skills/mobile-api-network-attacker/SKILL.md +421 -0
- package/skills/mobile-binary-hardener/SKILL.md +102 -0
- package/skills/mobile-security-specialist/SKILL.md +85 -0
- package/skills/mobile-webview-auditor/SKILL.md +96 -0
- package/skills/model-extraction-attacker/SKILL.md +219 -0
- package/skills/multipart-abuse-tester/SKILL.md +84 -0
- package/skills/oauth-pkce-specialist/SKILL.md +104 -0
- package/skills/parser-exhaustion-tester/SKILL.md +142 -0
- package/skills/pentest-infra/SKILL.md +141 -0
- package/skills/pentest-social/SKILL.md +201 -0
- package/skills/pentest-team/SKILL.md +134 -0
- package/skills/pentest-web-api/SKILL.md +151 -0
- package/skills/privacy-flow-analyst/SKILL.md +234 -0
- package/skills/prompt-injection-specialist/SKILL.md +394 -0
- package/skills/quantum-migration-planner/SKILL.md +96 -0
- package/skills/rag-poisoning-specialist/SKILL.md +358 -0
- package/skills/registry-mirror-enforcer/SKILL.md +84 -0
- package/skills/rotation-validation-agent/SKILL.md +112 -0
- package/skills/samm-assessor/SKILL.md +85 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
- package/skills/senior-security-engineer/SKILL.md +370 -2
- package/skills/serialization-memory-attacker/SKILL.md +332 -0
- package/skills/session-timeout-tester/SKILL.md +161 -0
- package/skills/slsa-level3-enforcer/SKILL.md +112 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
- package/skills/ssrf-detection-validator/SKILL.md +108 -0
- package/skills/step-up-auth-enforcer/SKILL.md +84 -0
- package/skills/stride-pasta-analyst/SKILL.md +420 -0
- package/skills/supply-chain-devsecops/SKILL.md +98 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
- package/skills/threat-modeler/SKILL.md +85 -0
- package/skills/tls-certificate-auditor/SKILL.md +573 -18
- package/skills/token-reuse-detector/SKILL.md +95 -0
- package/skills/trike-risk-modeler/SKILL.md +84 -0
- package/skills/unicode-homograph-tester/SKILL.md +84 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
- package/skills/webhook-security-tester/SKILL.md +102 -0
- package/skills/zero-trust-architect/SKILL.md +109 -0
package/dist/gate/diff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
2
3
|
// Allowlist for git ref strings. Blocks option injection (e.g. --upload-pack=…)
|
|
3
4
|
// and git pathspec magic characters. CWE-88 / MITRE ATT&CK T1059.
|
|
4
5
|
// Note: ~ and ^ are intentionally included — they are safe because { and } are NOT
|
|
@@ -12,12 +13,23 @@ function validateRef(name, value) {
|
|
|
12
13
|
export async function getChangedFiles(opts) {
|
|
13
14
|
validateRef("baseRef", opts.baseRef);
|
|
14
15
|
validateRef("headRef", opts.headRef);
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return stdout
|
|
16
|
+
// Fix 9: --diff-filter=ACMRT excludes deleted-only files; -M detects renames
|
|
17
|
+
// so renamed files appear as renames rather than delete+add pairs.
|
|
18
|
+
const { stdout } = await execa("git", ["diff", "--diff-filter=ACMRT", "-M", "--name-only", `${opts.baseRef}...${opts.headRef}`], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19
|
+
const candidates = stdout
|
|
20
20
|
.split("\n")
|
|
21
21
|
.map((s) => s.trim())
|
|
22
22
|
.filter(Boolean);
|
|
23
|
+
// Fix 9: skip any file that no longer exists on disk (deleted/moved away edge cases)
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const file of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
await access(file);
|
|
28
|
+
results.push(file);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// file does not exist on disk — skip gracefully
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
23
35
|
}
|
package/dist/gate/evidence.js
CHANGED
|
@@ -8,7 +8,14 @@ const PKG_ROOT = resolve(__dirname, "../..");
|
|
|
8
8
|
async function loadEvidenceMap() {
|
|
9
9
|
const overridePath = process.env["SECURITY_GATE_EVIDENCE_MAP"];
|
|
10
10
|
if (overridePath) {
|
|
11
|
-
|
|
11
|
+
// Guard against path traversal (VULN-002 / CWE-22): join() normalises '..' sequences
|
|
12
|
+
// but does NOT prevent escape; resolve() + startsWith() is the correct check.
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const resolved = resolve(cwd, overridePath);
|
|
15
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + "/")) {
|
|
16
|
+
throw new Error("SECURITY_GATE_EVIDENCE_MAP path escapes the project directory");
|
|
17
|
+
}
|
|
18
|
+
const raw = await readFile(resolved, "utf-8");
|
|
12
19
|
return JSON.parse(raw);
|
|
13
20
|
}
|
|
14
21
|
try {
|
package/dist/gate/exceptions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -13,13 +14,51 @@ const ExceptionSchema = z.object({
|
|
|
13
14
|
owner: z.string(),
|
|
14
15
|
approver: z.string(),
|
|
15
16
|
approval_role: z.string(),
|
|
17
|
+
// Fix 1: enforce YYYY-MM-DD format and max 365-day TTL
|
|
16
18
|
expires_on: z.string()
|
|
19
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "expires_on must be YYYY-MM-DD")
|
|
20
|
+
.refine((val) => {
|
|
21
|
+
const expiry = new Date(val);
|
|
22
|
+
const maxExpiry = new Date();
|
|
23
|
+
maxExpiry.setDate(maxExpiry.getDate() + 365);
|
|
24
|
+
return expiry <= maxExpiry;
|
|
25
|
+
}, { message: "expires_on cannot be more than 365 days in the future" })
|
|
26
|
+
})
|
|
27
|
+
// Fix 2: prevent self-approval (owner === approver)
|
|
28
|
+
.superRefine((data, ctx) => {
|
|
29
|
+
if (data.owner.toLowerCase() === data.approver.toLowerCase()) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: z.ZodIssueCode.custom,
|
|
32
|
+
message: `Exception self-approval is not permitted: owner and approver must be different (got '${data.owner}')`,
|
|
33
|
+
path: ["approver"]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
17
36
|
});
|
|
18
37
|
const ExceptionFileSchema = z.object({
|
|
19
38
|
version: z.string(),
|
|
20
|
-
exceptions: z.array(ExceptionSchema).default([])
|
|
39
|
+
exceptions: z.array(ExceptionSchema).default([]),
|
|
40
|
+
hmacSha256: z.string().optional()
|
|
21
41
|
});
|
|
42
|
+
// Fix 3: HMAC helper — sign the exceptions array for tamper detection
|
|
43
|
+
export function signExceptionsFile(exceptions, key) {
|
|
44
|
+
const canonical = JSON.stringify(exceptions, (_, v) => (v && typeof v === "object" && !Array.isArray(v)
|
|
45
|
+
? Object.fromEntries(Object.entries(v).sort())
|
|
46
|
+
: v));
|
|
47
|
+
return createHmac("sha256", key).update(canonical, "utf-8").digest("hex");
|
|
48
|
+
}
|
|
49
|
+
const EXCEPTIONS_HMAC_MIN_KEY_BYTES = 32;
|
|
50
|
+
function getExceptionsHmacKey() {
|
|
51
|
+
const key = process.env["SECURITY_POLICY_HMAC_KEY"];
|
|
52
|
+
if (!key)
|
|
53
|
+
return null;
|
|
54
|
+
if (Buffer.byteLength(key, "utf-8") < EXCEPTIONS_HMAC_MIN_KEY_BYTES) {
|
|
55
|
+
throw new Error(`SECURITY_POLICY_HMAC_KEY is too short (${Buffer.byteLength(key, "utf-8")} bytes). ` +
|
|
56
|
+
`Minimum ${EXCEPTIONS_HMAC_MIN_KEY_BYTES} bytes required.`);
|
|
57
|
+
}
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
22
60
|
async function readExceptionsJson() {
|
|
61
|
+
const warnings = [];
|
|
23
62
|
const overridePath = process.env["SECURITY_GATE_EXCEPTIONS"];
|
|
24
63
|
if (overridePath) {
|
|
25
64
|
// CWE-22: ensure path stays within the project directory
|
|
@@ -27,21 +66,89 @@ async function readExceptionsJson() {
|
|
|
27
66
|
if (!resolved.startsWith(process.cwd() + "/") && resolved !== process.cwd()) {
|
|
28
67
|
throw new Error(`SECURITY_GATE_EXCEPTIONS path '${overridePath}' escapes the project directory`);
|
|
29
68
|
}
|
|
30
|
-
|
|
69
|
+
const raw = await readFile(resolved, "utf-8");
|
|
70
|
+
return { raw, isCiFile: false, warnings };
|
|
71
|
+
}
|
|
72
|
+
// Project-level CI exceptions file (suppresses self-scan false positives)
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readFile(join(process.cwd(), ".github", "security-exceptions-ci.json"), "utf-8");
|
|
75
|
+
// Fix 4: warn when CI exceptions are loaded outside CI context
|
|
76
|
+
const isCI = !!(process.env["CI"] || process.env["GITHUB_ACTIONS"]);
|
|
77
|
+
if (!isCI) {
|
|
78
|
+
const count = (() => {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
return Array.isArray(parsed.exceptions) ? parsed.exceptions.length : 0;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
warnings.push({
|
|
88
|
+
id: "CI_EXCEPTIONS_IN_LOCAL_SCAN",
|
|
89
|
+
title: "CI self-scan exceptions applied to local scan",
|
|
90
|
+
severity: "HIGH",
|
|
91
|
+
evidence: [
|
|
92
|
+
"CI exceptions file: .github/security-exceptions-ci.json",
|
|
93
|
+
`Suppressed controls: ${count}`,
|
|
94
|
+
"CI env var not set — this appears to be a local scan"
|
|
95
|
+
],
|
|
96
|
+
requiredActions: [
|
|
97
|
+
`CI self-scan exceptions (.github/security-exceptions-ci.json) are being applied to a local scan. This suppresses ${count} controls. Set SECURITY_GATE_EXCEPTIONS to point to your project's exceptions file.`
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { raw, isCiFile: true, warnings };
|
|
31
102
|
}
|
|
103
|
+
catch { /* not present — continue */ }
|
|
32
104
|
try {
|
|
33
|
-
|
|
105
|
+
const raw = await readFile(join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json"), "utf-8");
|
|
106
|
+
return { raw, isCiFile: false, warnings };
|
|
34
107
|
}
|
|
35
108
|
catch {
|
|
36
|
-
|
|
109
|
+
const raw = await readFile(join(PKG_ROOT, "defaults", "security-exceptions.json"), "utf-8");
|
|
110
|
+
return { raw, isCiFile: false, warnings };
|
|
37
111
|
}
|
|
38
112
|
}
|
|
39
113
|
export async function loadSecurityExceptions() {
|
|
40
|
-
const raw = await readExceptionsJson();
|
|
41
|
-
|
|
114
|
+
const { raw, warnings } = await readExceptionsJson();
|
|
115
|
+
const parsed = ExceptionFileSchema.parse(JSON.parse(raw));
|
|
116
|
+
// Fix 3: HMAC verification of exceptions file
|
|
117
|
+
const hmacKey = getExceptionsHmacKey();
|
|
118
|
+
const extraWarnings = [];
|
|
119
|
+
if (hmacKey) {
|
|
120
|
+
if (!parsed.hmacSha256) {
|
|
121
|
+
extraWarnings.push({
|
|
122
|
+
id: "EXCEPTIONS_FILE_UNSIGNED",
|
|
123
|
+
title: "Security exceptions file is not integrity-protected",
|
|
124
|
+
severity: "MEDIUM",
|
|
125
|
+
evidence: [
|
|
126
|
+
"SECURITY_POLICY_HMAC_KEY is set but exceptions file has no hmacSha256 field"
|
|
127
|
+
],
|
|
128
|
+
requiredActions: [
|
|
129
|
+
"Security exceptions file is not integrity-protected. Set SECURITY_POLICY_HMAC_KEY and re-save exceptions to enable tamper detection."
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const expected = signExceptionsFile(parsed.exceptions, hmacKey);
|
|
135
|
+
const storedBuf = Buffer.from(parsed.hmacSha256, "hex");
|
|
136
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
137
|
+
const valid = storedBuf.length === expectedBuf.length &&
|
|
138
|
+
timingSafeEqual(storedBuf, expectedBuf);
|
|
139
|
+
if (!valid) {
|
|
140
|
+
throw new Error("[loadSecurityExceptions] HMAC verification failed for exceptions file — file may have been tampered. " +
|
|
141
|
+
"Re-sign exceptions with the signExceptionsFile helper and store the result in hmacSha256.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
exceptions: parsed.exceptions,
|
|
147
|
+
warnings: [...warnings, ...extraWarnings]
|
|
148
|
+
};
|
|
42
149
|
}
|
|
43
|
-
export async function applySecurityExceptions(findings) {
|
|
44
|
-
const exceptions = await loadSecurityExceptions();
|
|
150
|
+
export async function applySecurityExceptions(findings, opts) {
|
|
151
|
+
const { exceptions, warnings } = await loadSecurityExceptions();
|
|
45
152
|
const active = [];
|
|
46
153
|
const suppressed = [];
|
|
47
154
|
const exceptionFindings = [];
|
|
@@ -75,6 +182,20 @@ export async function applySecurityExceptions(findings) {
|
|
|
75
182
|
});
|
|
76
183
|
continue;
|
|
77
184
|
}
|
|
185
|
+
// Fix 7: enforce require_ticket when set in policy
|
|
186
|
+
if (opts?.requireTicket && !match.ticket) {
|
|
187
|
+
active.push(finding);
|
|
188
|
+
exceptionFindings.push({
|
|
189
|
+
id: "EXCEPTION_MISSING_TICKET",
|
|
190
|
+
title: `Exception ${match.id} is missing a required ticket reference`,
|
|
191
|
+
severity: "MEDIUM",
|
|
192
|
+
evidence: [`Finding: ${finding.id}`, `Exception: ${match.id}`, `Owner: ${match.owner}`],
|
|
193
|
+
requiredActions: [
|
|
194
|
+
`Exception ${match.id} for finding ${finding.id} is missing a required ticket reference. Set require_ticket: false in policy or add a ticket field.`
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
78
199
|
suppressed.push({
|
|
79
200
|
finding,
|
|
80
201
|
exceptionId: match.id,
|
|
@@ -85,6 +206,7 @@ export async function applySecurityExceptions(findings) {
|
|
|
85
206
|
findings: active,
|
|
86
207
|
suppressed,
|
|
87
208
|
exceptionFindings,
|
|
88
|
-
activeControlExceptionIds: Array.from(activeControlExceptionIds)
|
|
209
|
+
activeControlExceptionIds: Array.from(activeControlExceptionIds),
|
|
210
|
+
warnings
|
|
89
211
|
};
|
|
90
212
|
}
|