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
|
@@ -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
|
|
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
|
}
|