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.
Files changed (133) hide show
  1. package/README.md +164 -185
  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/control-catalog.json +200 -0
  9. package/defaults/security-policy.json +2 -2
  10. package/dist/cli/index.js +82 -5
  11. package/dist/cli/install.js +36 -6
  12. package/dist/cli/onboarding.js +6 -0
  13. package/dist/gate/baseline.js +82 -7
  14. package/dist/gate/catalog.js +10 -2
  15. package/dist/gate/checks/ai.js +757 -39
  16. package/dist/gate/checks/auth-deep.js +935 -0
  17. package/dist/gate/checks/business-logic.js +751 -0
  18. package/dist/gate/checks/ci-pipeline.js +399 -4
  19. package/dist/gate/checks/crypto.js +423 -2
  20. package/dist/gate/checks/dependencies.js +571 -15
  21. package/dist/gate/checks/graphql.js +201 -19
  22. package/dist/gate/checks/infra.js +246 -1
  23. package/dist/gate/checks/injection-deep.js +848 -0
  24. package/dist/gate/checks/k8s.js +114 -1
  25. package/dist/gate/checks/mobile-android.js +917 -3
  26. package/dist/gate/checks/mobile-ios.js +797 -5
  27. package/dist/gate/checks/required-artifacts.js +194 -0
  28. package/dist/gate/checks/runtime.js +178 -0
  29. package/dist/gate/checks/secrets.js +244 -13
  30. package/dist/gate/checks/supply-chain-deep.js +787 -0
  31. package/dist/gate/checks/web-nextjs.js +572 -48
  32. package/dist/gate/diff.js +17 -5
  33. package/dist/gate/evidence.js +8 -1
  34. package/dist/gate/exceptions.js +131 -9
  35. package/dist/gate/policy.js +282 -129
  36. package/dist/mcp/audit-chain.js +122 -28
  37. package/dist/mcp/auth.js +169 -0
  38. package/dist/mcp/learning.js +129 -4
  39. package/dist/mcp/model-router.js +158 -21
  40. package/dist/mcp/orchestration.js +186 -51
  41. package/dist/mcp/server.js +608 -94
  42. package/dist/repo/fs.js +24 -1
  43. package/dist/repo/search.js +31 -6
  44. package/dist/review/store.js +52 -1
  45. package/package.json +7 -7
  46. package/prompts/SECURITY_PROMPT.md +73 -0
  47. package/skills/_TEMPLATE/SKILL.md +99 -0
  48. package/skills/advanced-dos-tester/SKILL.md +109 -0
  49. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  50. package/skills/ai-llm-redteam/SKILL.md +104 -0
  51. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  52. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  53. package/skills/android-penetration-tester/SKILL.md +455 -46
  54. package/skills/anti-replay-tester/SKILL.md +106 -0
  55. package/skills/appsec-code-auditor/SKILL.md +120 -0
  56. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  57. package/skills/attack-navigator/SKILL.md +467 -8
  58. package/skills/auth-session-hacker/SKILL.md +128 -0
  59. package/skills/aws-penetration-tester/SKILL.md +456 -0
  60. package/skills/azure-penetration-tester/SKILL.md +490 -3
  61. package/skills/binary-auth-validator/SKILL.md +111 -0
  62. package/skills/bot-detection-specialist/SKILL.md +109 -0
  63. package/skills/business-logic-attacker/SKILL.md +231 -0
  64. package/skills/capec-code-mapper/SKILL.md +84 -0
  65. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  66. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  67. package/skills/ciso-orchestrator/SKILL.md +454 -43
  68. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  69. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  70. package/skills/compliance-grc/SKILL.md +85 -0
  71. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  72. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  73. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  74. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  75. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  76. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  77. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  78. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  79. package/skills/dos-resilience-tester/SKILL.md +97 -0
  80. package/skills/dread-scorer/SKILL.md +84 -0
  81. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  82. package/skills/evidence-collector/SKILL.md +98 -0
  83. package/skills/file-upload-attacker/SKILL.md +109 -0
  84. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  85. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  86. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  87. package/skills/incident-responder/SKILL.md +111 -0
  88. package/skills/injection-specialist/SKILL.md +131 -0
  89. package/skills/ios-security-auditor/SKILL.md +282 -0
  90. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  91. package/skills/k8s-container-escaper/SKILL.md +384 -0
  92. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  93. package/skills/kill-switch-engineer/SKILL.md +102 -0
  94. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  95. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  96. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  97. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  98. package/skills/mobile-security-specialist/SKILL.md +85 -0
  99. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  100. package/skills/model-extraction-attacker/SKILL.md +219 -0
  101. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  102. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  103. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  104. package/skills/pentest-infra/SKILL.md +141 -0
  105. package/skills/pentest-social/SKILL.md +201 -0
  106. package/skills/pentest-team/SKILL.md +134 -0
  107. package/skills/pentest-web-api/SKILL.md +151 -0
  108. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  109. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  110. package/skills/quantum-migration-planner/SKILL.md +96 -0
  111. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  112. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  113. package/skills/rotation-validation-agent/SKILL.md +112 -0
  114. package/skills/samm-assessor/SKILL.md +85 -0
  115. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  116. package/skills/senior-security-engineer/SKILL.md +370 -2
  117. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  118. package/skills/session-timeout-tester/SKILL.md +161 -0
  119. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  120. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  121. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  122. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  123. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  124. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  125. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  126. package/skills/threat-modeler/SKILL.md +85 -0
  127. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  128. package/skills/token-reuse-detector/SKILL.md +95 -0
  129. package/skills/trike-risk-modeler/SKILL.md +84 -0
  130. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  131. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  132. package/skills/webhook-security-tester/SKILL.md +102 -0
  133. package/skills/zero-trust-architect/SKILL.md +109 -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,69 @@ 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
+ // Base64 candidates: length >= 20, valid base64 chars
185
+ const b64Regex = /[A-Za-z0-9+/]{20,}={0,2}/g;
186
+ let b64Match;
187
+ while ((b64Match = b64Regex.exec(text)) !== null) {
188
+ const candidate = b64Match[0];
189
+ try {
190
+ const decoded = Buffer.from(candidate, "base64").toString("utf8");
191
+ // Only proceed if decoded output looks like printable ASCII (avoid false positives on binary)
192
+ if (!/^[\x20-\x7E\t\r\n]{8,}$/.test(decoded))
193
+ continue;
194
+ const hit = matchSecretPatterns(decoded);
195
+ if (hit) {
196
+ const preview = previewLine(text, b64Match.index);
197
+ encodingHits.push(`${file}: base64-encoded ${hit.name} detected — encoded="${candidate.slice(0, 40)}…" decoded_match="[REDACTED]" context="${preview.slice(0, 80)}"`);
198
+ }
199
+ }
200
+ catch {
201
+ // decode failed — skip
202
+ }
203
+ }
204
+ // Hex candidates: length >= 32, even number of hex chars
205
+ const hexRegex = /\b[0-9a-fA-F]{32,}\b/g;
206
+ let hexMatch;
207
+ while ((hexMatch = hexRegex.exec(text)) !== null) {
208
+ const candidate = hexMatch[0];
209
+ if (candidate.length % 2 !== 0)
210
+ continue;
211
+ try {
212
+ const decoded = Buffer.from(candidate, "hex").toString("utf8");
213
+ if (!/^[\x20-\x7E\t\r\n]{8,}$/.test(decoded))
214
+ continue;
215
+ const hit = matchSecretPatterns(decoded);
216
+ if (hit) {
217
+ const preview = previewLine(text, hexMatch.index);
218
+ encodingHits.push(`${file}: hex-encoded ${hit.name} detected — encoded="${candidate.slice(0, 40)}…" decoded_match="[REDACTED]" context="${preview.slice(0, 80)}"`);
219
+ }
220
+ }
221
+ catch {
222
+ // decode failed — skip
223
+ }
224
+ }
111
225
  }
226
+ // ------------------------------------------------------------------
227
+ // Emit findings for primary pattern hits
228
+ // ------------------------------------------------------------------
112
229
  for (const [patternName, hits] of hitsByPattern) {
113
230
  const pattern = SECRET_PATTERNS.find((p) => p.name === patternName);
114
231
  const description = pattern?.description ?? patternName;
@@ -126,5 +243,119 @@ export async function checkSecrets(_) {
126
243
  ]
127
244
  });
128
245
  }
246
+ // ------------------------------------------------------------------
247
+ // Emit findings for encoding evasion hits
248
+ // ------------------------------------------------------------------
249
+ if (encodingHits.length > 0) {
250
+ findings.push({
251
+ id: "ENCODED_SECRET",
252
+ title: "Encoded secret detected (base64 or hex evasion)",
253
+ severity: "CRITICAL",
254
+ files: [...new Set(encodingHits.map((h) => h.split(":")[0]).filter(Boolean))],
255
+ evidence: encodingHits.slice(0, 10),
256
+ requiredActions: [
257
+ "Encoded secrets are still secrets — encoding is not encryption.",
258
+ "Decode and rotate any exposed credentials immediately.",
259
+ "Remove the encoded value from source code and use a secret manager instead."
260
+ ]
261
+ });
262
+ }
263
+ // ------------------------------------------------------------------
264
+ // Emit findings for concatenation heuristic hits
265
+ // ------------------------------------------------------------------
266
+ if (concatHits.length > 0) {
267
+ findings.push({
268
+ id: "SECRET_CONCATENATION_SUSPICIOUS",
269
+ title: "Suspicious secret concatenation or split-string obfuscation detected",
270
+ severity: "MEDIUM",
271
+ files: [...new Set(concatHits.map((h) => h.split(":")[0]).filter(Boolean))],
272
+ evidence: concatHits.slice(0, 10),
273
+ requiredActions: [
274
+ "Review concatenated string assignments near secret-keyword variable names.",
275
+ "Split-string obfuscation does not prevent extraction — treat as a hardcoded secret.",
276
+ "Move the value to a secret manager and reference it via environment variable."
277
+ ]
278
+ });
279
+ }
280
+ // ------------------------------------------------------------------
281
+ // Fix 7: Git history scan via gitleaks
282
+ // ------------------------------------------------------------------
283
+ let gitleaksAvailable = false;
284
+ try {
285
+ await execFileAsync("gitleaks", ["version"]);
286
+ gitleaksAvailable = true;
287
+ }
288
+ catch {
289
+ gitleaksAvailable = false;
290
+ }
291
+ if (!gitleaksAvailable) {
292
+ findings.push({
293
+ id: "GITLEAKS_NOT_IN_PATH",
294
+ title: "git history not scanned — gitleaks binary not found",
295
+ severity: "MEDIUM",
296
+ files: [],
297
+ evidence: ["gitleaks was not found in PATH; git history secrets scan was skipped"],
298
+ requiredActions: [
299
+ "Install gitleaks (https://github.com/gitleaks/gitleaks) to enable git history scanning.",
300
+ "Run: gitleaks detect --source . --log-opts='--all' to scan full commit history.",
301
+ "Secrets committed in the past and later removed are still exposed in git history."
302
+ ]
303
+ });
304
+ }
305
+ else {
306
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${randomBytes(8).toString("hex")}.json`);
307
+ try {
308
+ await execFileAsync("gitleaks", [
309
+ "detect",
310
+ "--source", ".",
311
+ "--log-opts=--all",
312
+ "--no-git=false",
313
+ "--exit-code", "1",
314
+ "--report-format", "json",
315
+ "--report-path", tmpReport
316
+ ]);
317
+ // exit code 0 — no findings
318
+ }
319
+ catch {
320
+ // exit code 1 means findings were found; report file should exist
321
+ }
322
+ try {
323
+ if (existsSync(tmpReport)) {
324
+ const raw = readFileSync(tmpReport, "utf8");
325
+ const leaksData = JSON.parse(raw);
326
+ if (Array.isArray(leaksData) && leaksData.length > 0) {
327
+ const evidence = leaksData.slice(0, 20).map((leak) => {
328
+ const commit = leak.Commit ? leak.Commit.slice(0, 8) : "unknown";
329
+ const file = leak.File ?? "unknown";
330
+ const rule = leak.RuleID ?? leak.Description ?? "unknown";
331
+ return `commit=${commit} file=${file} rule=${rule}`;
332
+ });
333
+ const uniqueFiles = [...new Set(leaksData.map((l) => l.File ?? "unknown").filter(Boolean))];
334
+ findings.push({
335
+ id: "GIT_HISTORY_SECRET",
336
+ title: `Secret detected in git history (${leaksData.length} finding${leaksData.length === 1 ? "" : "s"})`,
337
+ severity: "HIGH",
338
+ files: uniqueFiles,
339
+ evidence,
340
+ requiredActions: [
341
+ "Secrets in git history remain exposed even after removal from the working tree.",
342
+ "Rotate all exposed credentials immediately.",
343
+ "Use git-filter-repo or BFG Repo-Cleaner to purge the secrets from history, then force-push and notify all collaborators to re-clone.",
344
+ "Enable branch protection and secret scanning alerts on the remote host."
345
+ ]
346
+ });
347
+ }
348
+ }
349
+ }
350
+ catch {
351
+ // report parse failure — non-fatal
352
+ }
353
+ finally {
354
+ try {
355
+ await unlink(tmpReport);
356
+ }
357
+ catch { }
358
+ }
359
+ }
129
360
  return findings;
130
361
  }