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.
Files changed (158) hide show
  1. package/README.md +341 -1018
  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/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -1,5 +1,13 @@
1
1
  import fg from "fast-glob";
2
2
  import { readFileSafe } from "../../repo/fs.js";
3
+ import { execFile } from "child_process";
4
+ import { promisify } from "util";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { unlink } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
+ const execFileAsync = promisify(execFile);
3
11
  const SECRET_PATTERNS = [
4
12
  // Private keys
5
13
  { name: "private_key_pem", regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, description: "PEM private key" },
@@ -18,6 +26,7 @@ const SECRET_PATTERNS = [
18
26
  { name: "azure_sas_token", regex: /\bsig=[A-Za-z0-9%+/]{43,}%3D/, description: "Azure SAS token" },
19
27
  { name: "azure_client_secret", regex: /\bAZURE_CLIENT_SECRET\s*[:=]\s*["'][^"'\n]{20,}["']/, description: "Azure client secret" },
20
28
  { name: "azure_subscription_key", regex: /\bOcp-Apim-Subscription-Key\s*[:=]\s*["'][0-9a-f]{32}["']/, description: "Azure APIM subscription key" },
29
+ { name: "arm_client_secret", regex: /\bARM_CLIENT_SECRET\s*[:=]\s*['"][^'"]{20,}['"]/, description: "Terraform Azure ARM client secret" },
21
30
  // GitHub / GitLab / Bitbucket
22
31
  { name: "github_personal_token", regex: /\bghp_[A-Za-z0-9]{36}\b/, description: "GitHub personal access token" },
23
32
  { name: "github_oauth_token", regex: /\bgho_[A-Za-z0-9]{36}\b/, description: "GitHub OAuth token" },
@@ -39,12 +48,13 @@ const SECRET_PATTERNS = [
39
48
  // Communication
40
49
  { name: "twilio_account_sid", regex: /\bAC[a-fA-F0-9]{32}\b/, description: "Twilio account SID" },
41
50
  { name: "twilio_auth_token", regex: /\bTWILIO_AUTH_TOKEN\s*[:=]\s*["'][a-fA-F0-9]{32}["']/, description: "Twilio auth token" },
51
+ { name: "twilio_token_positional", regex: /new\s+(?:Twilio|twilio)\s*\([^,]+,\s*['"]([A-Fa-f0-9]{32})['"]/, description: "Twilio auth token (positional constructor)" },
42
52
  { name: "sendgrid_api_key", regex: /\bSG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}\b/, description: "SendGrid API key" },
43
53
  { name: "mailgun_api_key", regex: /\bkey-[A-Za-z0-9]{32}\b/, description: "Mailgun API key" },
44
54
  // LLM / AI providers
45
55
  { name: "openai_api_key", regex: /\bsk-[A-Za-z0-9]{20,}\b/, description: "OpenAI API key" },
46
56
  { name: "anthropic_api_key", regex: /\bsk-ant-[A-Za-z0-9\-_]{40,}\b/, description: "Anthropic API key" },
47
- { name: "huggingface_token", regex: /\bhf_[A-Za-z0-9]{34}\b/, description: "HuggingFace token" },
57
+ { name: "huggingface_token", regex: /\bhf_[A-Za-z0-9]{34,}\b/, description: "HuggingFace token" },
48
58
  { name: "cohere_api_key", regex: /\bCOHERE_API_KEY\s*[:=]\s*["'][A-Za-z0-9]{40}["']/, description: "Cohere API key" },
49
59
  // Database connection strings with embedded credentials
50
60
  { name: "db_connection_string", regex: /(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|mssql):\/\/[^:]+:[^@\s]{6,}@/, description: "Database connection string with embedded credentials" },
@@ -52,10 +62,20 @@ const SECRET_PATTERNS = [
52
62
  // Infrastructure tokens
53
63
  { name: "hashicorp_vault_token", regex: /\bhvs\.[A-Za-z0-9]{24,}\b/, description: "HashiCorp Vault service token" },
54
64
  { name: "npm_token", regex: /\bnpm_[A-Za-z0-9]{36}\b/, description: "npm access token" },
65
+ { name: "npmrc_auth_token", regex: /_authToken\s*=\s*[A-Za-z0-9_\-.]{10,}/, description: "npm _authToken in .npmrc" },
55
66
  { name: "docker_hub_pat", regex: /\bdckr_pat_[A-Za-z0-9\-_]{27}\b/, description: "Docker Hub personal access token" },
56
67
  { name: "terraform_cloud_token", regex: /\b[A-Za-z0-9]{14}\.atlasv1\.[A-Za-z0-9]{60,}\b/, description: "Terraform Cloud token" },
57
68
  { name: "datadog_api_key", regex: /\bDD_API_KEY\s*[:=]\s*["'][a-fA-F0-9]{32}["']/, description: "Datadog API key" },
58
69
  { name: "new_relic_key", regex: /\bNEW_RELIC_LICENSE_KEY\s*[:=]\s*["'][A-Za-z0-9]{40}["']/, description: "New Relic license key" },
70
+ // SaaS / Cloud platform tokens
71
+ { name: "vercel_token", regex: /\bvercel_[A-Za-z0-9]{20,}\b/, description: "Vercel token" },
72
+ { name: "planetscale_token", regex: /\bpscale_tkn_[A-Za-z0-9_]{20,}\b/, description: "PlanetScale token" },
73
+ { name: "databricks_token", regex: /\bdapi[a-fA-F0-9]{32}\b/, description: "Databricks API token" },
74
+ { name: "linear_api_key", regex: /\blin_api_[A-Za-z0-9]{20,}\b/, description: "Linear API key" },
75
+ { name: "doppler_token", regex: /\bdp\.st\.[a-zA-Z0-9.]+\b/, description: "Doppler service token" },
76
+ { name: "railway_token", regex: /\bRW_[A-Za-z0-9]{20,}\b/, description: "Railway token" },
77
+ // process.env fallback with hardcoded secret
78
+ { name: "env_fallback_hardcoded", regex: /process\.env\.\w+\s*(?:\?\?|\|\|)\s*['"][^'"]{16,}['"]/, description: "process.env fallback with hardcoded secret value" },
59
79
  // Generic high-confidence patterns
60
80
  { name: "secret_key_assignment", regex: /\b(?:SECRET|API)_KEY\s*[:=]\s*["'][^"'\n]{16,}["']/, description: "Generic secret/API key assignment" },
61
81
  { name: "password_assignment", regex: /\b(?:PASSWORD|PASSWD|PWD)\s*[:=]\s*["'][^"'\n]{8,}["']/, description: "Hardcoded password assignment" },
@@ -67,25 +87,57 @@ function previewLine(text, index) {
67
87
  const lineEnd = text.indexOf("\n", index);
68
88
  return text.slice(lineStart === -1 ? 0 : lineStart + 1, lineEnd === -1 ? undefined : lineEnd).trim();
69
89
  }
90
+ /** Scan decoded text against all SECRET_PATTERNS; returns first match name or null */
91
+ function matchSecretPatterns(decoded) {
92
+ for (const pattern of SECRET_PATTERNS) {
93
+ const m = pattern.regex.exec(decoded);
94
+ if (m)
95
+ return { name: pattern.name, match: m[0] };
96
+ }
97
+ return null;
98
+ }
70
99
  export async function checkSecrets(_) {
71
100
  const findings = [];
72
- const files = await fg(["**/*.*"], {
101
+ const IGNORE_LIST = [
102
+ "**/node_modules/**",
103
+ "**/.git/**",
104
+ "**/dist/**",
105
+ "**/fixtures/**",
106
+ "**/.mcp/reviews/**",
107
+ "**/.mcp/reports/**",
108
+ "**/.claude/**",
109
+ // Exclude detection source — contains regex patterns that match their own rules
110
+ "src/gate/checks/secrets.ts"
111
+ ];
112
+ const files = await fg(["**/*.*", "**/.*"], {
73
113
  dot: true,
74
114
  onlyFiles: true,
75
- ignore: [
76
- "**/node_modules/**",
77
- "**/.git/**",
78
- "**/dist/**",
79
- "**/fixtures/**",
80
- "**/.mcp/reviews/**",
81
- "**/.mcp/reports/**",
82
- "**/.claude/**",
83
- // Exclude detection source — contains regex patterns that match their own rules
84
- "src/gate/checks/secrets.ts"
85
- ]
115
+ ignore: IGNORE_LIST
86
116
  });
117
+ // ------------------------------------------------------------------
118
+ // Fix 8: Warn when dist/ exists but is excluded from scanning
119
+ // ------------------------------------------------------------------
120
+ const distExists = existsSync("dist") || existsSync("./dist");
121
+ if (distExists) {
122
+ findings.push({
123
+ id: "SECRET_DIST_NOT_SCANNED",
124
+ title: "Compiled dist/ directory excluded from secret scan",
125
+ severity: "LOW",
126
+ files: ["dist/"],
127
+ evidence: ["dist/ directory exists but is excluded from secret scanning"],
128
+ requiredActions: [
129
+ "Manually inspect dist/ for secrets injected by build tools such as webpack DefinePlugin or Vite define.",
130
+ "Ensure secrets are not inlined into compiled bundles via build-time substitution.",
131
+ "Consider adding a targeted scan of dist/ for high-confidence patterns (API key prefixes, PEM headers) in CI."
132
+ ]
133
+ });
134
+ }
87
135
  // Track hits per pattern so each type gets its own finding with specific guidance
88
136
  const hitsByPattern = new Map();
137
+ // Track encoding evasion hits separately
138
+ const encodingHits = [];
139
+ // Track concatenation hits separately
140
+ const concatHits = [];
89
141
  for (const file of files) {
90
142
  let text = "";
91
143
  try {
@@ -94,6 +146,9 @@ export async function checkSecrets(_) {
94
146
  catch {
95
147
  continue;
96
148
  }
149
+ // ------------------------------------------------------------------
150
+ // Primary scan: run all SECRET_PATTERNS against raw file content
151
+ // ------------------------------------------------------------------
97
152
  for (const pattern of SECRET_PATTERNS) {
98
153
  const match = pattern.regex.exec(text);
99
154
  if (!match || match.index === undefined)
@@ -108,7 +163,81 @@ export async function checkSecrets(_) {
108
163
  hitsByPattern.set(pattern.name, existing);
109
164
  }
110
165
  }
166
+ // ------------------------------------------------------------------
167
+ // Fix 6: Split-string / concatenation detection
168
+ // ------------------------------------------------------------------
169
+ const concatPatterns = [
170
+ /(?:apiKey|secret|token|password|key)\s*=\s*['"][^'"]{4,}['"]\s*\+/gi,
171
+ /(?:AKIA|sk_live_|sk-|ghp_|xoxb-)[\w+/]{4,}['"]\s*,[\s\S]{0,40}\.join\s*\(\s*['"]{2}\s*\)/gi,
172
+ ];
173
+ for (const cp of concatPatterns) {
174
+ const m = cp.exec(text);
175
+ if (m) {
176
+ const preview = previewLine(text, m.index);
177
+ concatHits.push(`${file}: ${preview.slice(0, 120)}`);
178
+ break; // one hit per file per pass is enough
179
+ }
180
+ }
181
+ // ------------------------------------------------------------------
182
+ // Fix 2: Encoding evasion — base64 and hex secondary pass
183
+ // ------------------------------------------------------------------
184
+ // SECURITY (CWE-400): a single multi-MB contiguous base64/hex run makes V8's
185
+ // regex engine throw RangeError ("Maximum call stack size exceeded"). The
186
+ // readFileSafe size cap bounds file size, but contain any residual throw here
187
+ // so one crafted repo file cannot crash the gate (docs tier) or silently drop
188
+ // all secret findings (full tier swallows the rejection via Promise.allSettled).
189
+ try {
190
+ // Base64 candidates: length >= 20, valid base64 chars
191
+ const b64Regex = /[A-Za-z0-9+/]{20,}={0,2}/g;
192
+ let b64Match;
193
+ while ((b64Match = b64Regex.exec(text)) !== null) {
194
+ const candidate = b64Match[0];
195
+ try {
196
+ const decoded = Buffer.from(candidate, "base64").toString("utf8");
197
+ // Only proceed if decoded output looks like printable ASCII (avoid false positives on binary)
198
+ if (!/^[\x20-\x7E\t\r\n]{8,}$/.test(decoded))
199
+ continue;
200
+ const hit = matchSecretPatterns(decoded);
201
+ if (hit) {
202
+ const preview = previewLine(text, b64Match.index);
203
+ encodingHits.push(`${file}: base64-encoded ${hit.name} detected — encoded="${candidate.slice(0, 40)}…" decoded_match="[REDACTED]" context="${preview.slice(0, 80)}"`);
204
+ }
205
+ }
206
+ catch {
207
+ // decode failed — skip
208
+ }
209
+ }
210
+ // Hex candidates: length >= 32, even number of hex chars
211
+ const hexRegex = /\b[0-9a-fA-F]{32,}\b/g;
212
+ let hexMatch;
213
+ while ((hexMatch = hexRegex.exec(text)) !== null) {
214
+ const candidate = hexMatch[0];
215
+ if (candidate.length % 2 !== 0)
216
+ continue;
217
+ try {
218
+ const decoded = Buffer.from(candidate, "hex").toString("utf8");
219
+ if (!/^[\x20-\x7E\t\r\n]{8,}$/.test(decoded))
220
+ continue;
221
+ const hit = matchSecretPatterns(decoded);
222
+ if (hit) {
223
+ const preview = previewLine(text, hexMatch.index);
224
+ encodingHits.push(`${file}: hex-encoded ${hit.name} detected — encoded="${candidate.slice(0, 40)}…" decoded_match="[REDACTED]" context="${preview.slice(0, 80)}"`);
225
+ }
226
+ }
227
+ catch {
228
+ // decode failed — skip
229
+ }
230
+ }
231
+ }
232
+ catch {
233
+ // CWE-400: regex engine RangeError or similar on a pathological file —
234
+ // skip this file's encoding pass rather than aborting the whole scan.
235
+ continue;
236
+ }
111
237
  }
238
+ // ------------------------------------------------------------------
239
+ // Emit findings for primary pattern hits
240
+ // ------------------------------------------------------------------
112
241
  for (const [patternName, hits] of hitsByPattern) {
113
242
  const pattern = SECRET_PATTERNS.find((p) => p.name === patternName);
114
243
  const description = pattern?.description ?? patternName;
@@ -126,5 +255,119 @@ export async function checkSecrets(_) {
126
255
  ]
127
256
  });
128
257
  }
258
+ // ------------------------------------------------------------------
259
+ // Emit findings for encoding evasion hits
260
+ // ------------------------------------------------------------------
261
+ if (encodingHits.length > 0) {
262
+ findings.push({
263
+ id: "ENCODED_SECRET",
264
+ title: "Encoded secret detected (base64 or hex evasion)",
265
+ severity: "CRITICAL",
266
+ files: [...new Set(encodingHits.map((h) => h.split(":")[0]).filter(Boolean))],
267
+ evidence: encodingHits.slice(0, 10),
268
+ requiredActions: [
269
+ "Encoded secrets are still secrets — encoding is not encryption.",
270
+ "Decode and rotate any exposed credentials immediately.",
271
+ "Remove the encoded value from source code and use a secret manager instead."
272
+ ]
273
+ });
274
+ }
275
+ // ------------------------------------------------------------------
276
+ // Emit findings for concatenation heuristic hits
277
+ // ------------------------------------------------------------------
278
+ if (concatHits.length > 0) {
279
+ findings.push({
280
+ id: "SECRET_CONCATENATION_SUSPICIOUS",
281
+ title: "Suspicious secret concatenation or split-string obfuscation detected",
282
+ severity: "MEDIUM",
283
+ files: [...new Set(concatHits.map((h) => h.split(":")[0]).filter(Boolean))],
284
+ evidence: concatHits.slice(0, 10),
285
+ requiredActions: [
286
+ "Review concatenated string assignments near secret-keyword variable names.",
287
+ "Split-string obfuscation does not prevent extraction — treat as a hardcoded secret.",
288
+ "Move the value to a secret manager and reference it via environment variable."
289
+ ]
290
+ });
291
+ }
292
+ // ------------------------------------------------------------------
293
+ // Fix 7: Git history scan via gitleaks
294
+ // ------------------------------------------------------------------
295
+ let gitleaksAvailable = false;
296
+ try {
297
+ await execFileAsync("gitleaks", ["version"]);
298
+ gitleaksAvailable = true;
299
+ }
300
+ catch {
301
+ gitleaksAvailable = false;
302
+ }
303
+ if (!gitleaksAvailable) {
304
+ findings.push({
305
+ id: "GITLEAKS_NOT_IN_PATH",
306
+ title: "git history not scanned — gitleaks binary not found",
307
+ severity: "MEDIUM",
308
+ files: [],
309
+ evidence: ["gitleaks was not found in PATH; git history secrets scan was skipped"],
310
+ requiredActions: [
311
+ "Install gitleaks (https://github.com/gitleaks/gitleaks) to enable git history scanning.",
312
+ "Run: gitleaks detect --source . --log-opts='--all' to scan full commit history.",
313
+ "Secrets committed in the past and later removed are still exposed in git history."
314
+ ]
315
+ });
316
+ }
317
+ else {
318
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${randomBytes(8).toString("hex")}.json`);
319
+ try {
320
+ await execFileAsync("gitleaks", [
321
+ "detect",
322
+ "--source", ".",
323
+ "--log-opts=--all",
324
+ "--no-git=false",
325
+ "--exit-code", "1",
326
+ "--report-format", "json",
327
+ "--report-path", tmpReport
328
+ ]);
329
+ // exit code 0 — no findings
330
+ }
331
+ catch {
332
+ // exit code 1 means findings were found; report file should exist
333
+ }
334
+ try {
335
+ if (existsSync(tmpReport)) {
336
+ const raw = readFileSync(tmpReport, "utf8");
337
+ const leaksData = JSON.parse(raw);
338
+ if (Array.isArray(leaksData) && leaksData.length > 0) {
339
+ const evidence = leaksData.slice(0, 20).map((leak) => {
340
+ const commit = leak.Commit ? leak.Commit.slice(0, 8) : "unknown";
341
+ const file = leak.File ?? "unknown";
342
+ const rule = leak.RuleID ?? leak.Description ?? "unknown";
343
+ return `commit=${commit} file=${file} rule=${rule}`;
344
+ });
345
+ const uniqueFiles = [...new Set(leaksData.map((l) => l.File ?? "unknown").filter(Boolean))];
346
+ findings.push({
347
+ id: "GIT_HISTORY_SECRET",
348
+ title: `Secret detected in git history (${leaksData.length} finding${leaksData.length === 1 ? "" : "s"})`,
349
+ severity: "HIGH",
350
+ files: uniqueFiles,
351
+ evidence,
352
+ requiredActions: [
353
+ "Secrets in git history remain exposed even after removal from the working tree.",
354
+ "Rotate all exposed credentials immediately.",
355
+ "Use git-filter-repo or BFG Repo-Cleaner to purge the secrets from history, then force-push and notify all collaborators to re-clone.",
356
+ "Enable branch protection and secret scanning alerts on the remote host."
357
+ ]
358
+ });
359
+ }
360
+ }
361
+ }
362
+ catch {
363
+ // report parse failure — non-fatal
364
+ }
365
+ finally {
366
+ try {
367
+ await unlink(tmpReport);
368
+ }
369
+ catch { /* ignore cleanup failure */ }
370
+ }
371
+ }
129
372
  return findings;
130
373
  }