security-mcp 1.1.4 → 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.
Files changed (129) hide show
  1. package/README.md +116 -264
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/security-policy.json +2 -2
  9. package/dist/cli/index.js +0 -0
  10. package/dist/gate/baseline.js +82 -7
  11. package/dist/gate/catalog.js +10 -2
  12. package/dist/gate/checks/ai.js +757 -39
  13. package/dist/gate/checks/auth-deep.js +920 -216
  14. package/dist/gate/checks/business-logic.js +751 -0
  15. package/dist/gate/checks/ci-pipeline.js +399 -4
  16. package/dist/gate/checks/crypto.js +423 -2
  17. package/dist/gate/checks/dependencies.js +571 -15
  18. package/dist/gate/checks/graphql.js +201 -19
  19. package/dist/gate/checks/infra.js +246 -1
  20. package/dist/gate/checks/injection-deep.js +827 -184
  21. package/dist/gate/checks/k8s.js +114 -1
  22. package/dist/gate/checks/mobile-android.js +917 -3
  23. package/dist/gate/checks/mobile-ios.js +797 -5
  24. package/dist/gate/checks/required-artifacts.js +194 -0
  25. package/dist/gate/checks/runtime.js +178 -0
  26. package/dist/gate/checks/secrets.js +244 -13
  27. package/dist/gate/checks/supply-chain-deep.js +787 -0
  28. package/dist/gate/checks/web-nextjs.js +572 -48
  29. package/dist/gate/diff.js +17 -5
  30. package/dist/gate/evidence.js +8 -1
  31. package/dist/gate/exceptions.js +131 -9
  32. package/dist/gate/policy.js +280 -131
  33. package/dist/mcp/audit-chain.js +122 -28
  34. package/dist/mcp/auth.js +169 -0
  35. package/dist/mcp/learning.js +129 -4
  36. package/dist/mcp/model-router.js +158 -21
  37. package/dist/mcp/orchestration.js +186 -51
  38. package/dist/mcp/server.js +337 -53
  39. package/dist/repo/fs.js +24 -1
  40. package/dist/repo/search.js +31 -6
  41. package/dist/review/store.js +52 -1
  42. package/package.json +7 -7
  43. package/skills/_TEMPLATE/SKILL.md +99 -0
  44. package/skills/advanced-dos-tester/SKILL.md +109 -0
  45. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  46. package/skills/ai-llm-redteam/SKILL.md +104 -0
  47. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  48. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  49. package/skills/android-penetration-tester/SKILL.md +455 -46
  50. package/skills/anti-replay-tester/SKILL.md +106 -0
  51. package/skills/appsec-code-auditor/SKILL.md +85 -0
  52. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  53. package/skills/attack-navigator/SKILL.md +467 -8
  54. package/skills/auth-session-hacker/SKILL.md +102 -0
  55. package/skills/aws-penetration-tester/SKILL.md +456 -0
  56. package/skills/azure-penetration-tester/SKILL.md +490 -3
  57. package/skills/binary-auth-validator/SKILL.md +111 -0
  58. package/skills/bot-detection-specialist/SKILL.md +109 -0
  59. package/skills/business-logic-attacker/SKILL.md +231 -0
  60. package/skills/capec-code-mapper/SKILL.md +84 -0
  61. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  62. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  63. package/skills/ciso-orchestrator/SKILL.md +454 -43
  64. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  65. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  66. package/skills/compliance-grc/SKILL.md +85 -0
  67. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  68. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  69. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  70. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  71. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  75. package/skills/dos-resilience-tester/SKILL.md +97 -0
  76. package/skills/dread-scorer/SKILL.md +84 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  78. package/skills/evidence-collector/SKILL.md +98 -0
  79. package/skills/file-upload-attacker/SKILL.md +109 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  81. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  82. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  83. package/skills/incident-responder/SKILL.md +111 -0
  84. package/skills/injection-specialist/SKILL.md +102 -0
  85. package/skills/ios-security-auditor/SKILL.md +282 -0
  86. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  87. package/skills/k8s-container-escaper/SKILL.md +384 -0
  88. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  89. package/skills/kill-switch-engineer/SKILL.md +102 -0
  90. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  91. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  92. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  93. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  94. package/skills/mobile-security-specialist/SKILL.md +85 -0
  95. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  96. package/skills/model-extraction-attacker/SKILL.md +219 -0
  97. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  98. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  99. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  100. package/skills/pentest-infra/SKILL.md +98 -0
  101. package/skills/pentest-social/SKILL.md +201 -0
  102. package/skills/pentest-team/SKILL.md +87 -0
  103. package/skills/pentest-web-api/SKILL.md +98 -0
  104. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  105. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  106. package/skills/quantum-migration-planner/SKILL.md +96 -0
  107. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  108. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  109. package/skills/rotation-validation-agent/SKILL.md +112 -0
  110. package/skills/samm-assessor/SKILL.md +85 -0
  111. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  112. package/skills/senior-security-engineer/SKILL.md +167 -0
  113. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  114. package/skills/session-timeout-tester/SKILL.md +161 -0
  115. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  116. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  117. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  118. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  119. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  120. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  121. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  122. package/skills/threat-modeler/SKILL.md +85 -0
  123. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  124. package/skills/token-reuse-detector/SKILL.md +95 -0
  125. package/skills/trike-risk-modeler/SKILL.md +84 -0
  126. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  127. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  128. package/skills/webhook-security-tester/SKILL.md +102 -0
  129. 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
- // Uses git diff in CI. Assumes checkout has full history for baseRef.
16
- const { stdout } = await execa("git", ["diff", "--name-only", `${opts.baseRef}...${opts.headRef}`], {
17
- stdio: ["ignore", "pipe", "pipe"]
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
  }
@@ -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
- const raw = await readFile(join(process.cwd(), overridePath), "utf-8");
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 {
@@ -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
- return await readFile(resolved, "utf-8");
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
- return await readFile(join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json"), "utf-8");
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
- return await readFile(join(PKG_ROOT, "defaults", "security-exceptions.json"), "utf-8");
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
- return ExceptionFileSchema.parse(JSON.parse(raw)).exceptions;
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
  }