fullcourtdefense-cli 1.14.1 → 1.14.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/dist/commands/discoverSecrets.js +92 -27
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -38,29 +38,60 @@ const crypto = __importStar(require("crypto"));
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Patterns aligned with Gitleaks / TruffleHog / GitHub push-protection defaults
|
|
43
|
+
* (local-only, no network). ORDER MATTERS: specific prefixes (sk-proj-, sk-ant-,
|
|
44
|
+
* sk-or-) must come before the generic OpenAI sk- pattern — matches are deduped
|
|
45
|
+
* by the matched string, first pattern wins.
|
|
46
|
+
*/
|
|
42
47
|
const SECRET_PATTERNS = [
|
|
43
|
-
{ re: /\bsk-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI
|
|
44
|
-
{ re: /\bsk-
|
|
45
|
-
{ re: /\bsk-
|
|
46
|
-
{ re: /\
|
|
47
|
-
{ re: /\
|
|
48
|
-
{ re: /\
|
|
49
|
-
{ re: /\
|
|
50
|
-
{ re: /\
|
|
51
|
-
{ re: /\
|
|
52
|
-
{ re: /\
|
|
53
|
-
{ re: /\
|
|
54
|
-
{ re: /\
|
|
55
|
-
{ re: /\
|
|
56
|
-
{ re: /\
|
|
57
|
-
{ re: /\
|
|
58
|
-
{ re: /\
|
|
59
|
-
{ re: /\
|
|
60
|
-
{ re:
|
|
61
|
-
{ re:
|
|
62
|
-
{ re:
|
|
48
|
+
{ re: /\bsk-proj-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI project key', impact: 'full OpenAI API usage billed to your account' },
|
|
49
|
+
{ re: /\bsk-ant-[a-zA-Z0-9_-]{20,}\b/, provider: 'anthropic', severity: 'critical', title: 'Anthropic API key', impact: 'full Anthropic API usage billed to your account' },
|
|
50
|
+
{ re: /\bsk-or-[a-zA-Z0-9_-]{20,}\b/, provider: 'openrouter', severity: 'high', title: 'OpenRouter API key', impact: 'LLM usage across providers billed to your account' },
|
|
51
|
+
{ re: /\bsk-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI API key', impact: 'full OpenAI API usage billed to your account' },
|
|
52
|
+
{ re: /\bgsk_[a-zA-Z0-9]{20,}\b/, provider: 'groq', severity: 'high', title: 'Groq API key', impact: 'LLM inference billed to your account' },
|
|
53
|
+
{ re: /\bxai-[a-zA-Z0-9]{20,}\b/, provider: 'xai', severity: 'high', title: 'xAI API key', impact: 'LLM inference billed to your account' },
|
|
54
|
+
{ re: /\bghp_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub personal access token', impact: 'read/write access to your repositories' },
|
|
55
|
+
{ re: /\bgho_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub OAuth token', impact: 'read/write access to your repositories' },
|
|
56
|
+
{ re: /\bghs_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub app server token', impact: 'app-scoped repository access' },
|
|
57
|
+
{ re: /\bgithub_pat_[a-zA-Z0-9_]{20,}\b/, provider: 'github', severity: 'critical', title: 'GitHub fine-grained PAT', impact: 'scoped access to your repositories' },
|
|
58
|
+
{ re: /\bglpat-[a-zA-Z0-9_-]{20,}\b/, provider: 'gitlab', severity: 'critical', title: 'GitLab personal access token', impact: 'read/write access to your GitLab projects' },
|
|
59
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/, provider: 'aws', severity: 'critical', title: 'AWS access key ID', impact: 'AWS API access (paired with its secret key)' },
|
|
60
|
+
{ re: /\baws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}\b/i, provider: 'aws', severity: 'critical', title: 'AWS secret access key', impact: 'full AWS API access for the paired key ID' },
|
|
61
|
+
{ re: /\bAIza[0-9A-Za-z_-]{35}\b/, provider: 'google', severity: 'critical', title: 'Google API key', impact: 'Google Cloud API usage billed to your project' },
|
|
62
|
+
{ re: /\bxox[baprs]-[a-zA-Z0-9-]{10,}\b/, provider: 'slack', severity: 'high', title: 'Slack token', impact: 'read/post messages as your Slack app or user' },
|
|
63
|
+
{ re: /\bhooks\.slack\.com\/services\/T[a-zA-Z0-9]+\/B[a-zA-Z0-9]+\/[a-zA-Z0-9]+\b/, provider: 'slack', severity: 'high', title: 'Slack incoming webhook URL', impact: 'post arbitrary messages into your Slack channel' },
|
|
64
|
+
{ re: /\bsk_live_[0-9a-zA-Z]{24,}\b/, provider: 'stripe', severity: 'critical', title: 'Stripe live secret key', impact: 'charges, refunds, and customer data in live mode' },
|
|
65
|
+
{ re: /\brk_live_[0-9a-zA-Z]{24,}\b/, provider: 'stripe', severity: 'critical', title: 'Stripe restricted key', impact: 'scoped live-mode payment operations' },
|
|
66
|
+
{ re: /\bSG\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\b/, provider: 'sendgrid', severity: 'high', title: 'SendGrid API key', impact: 'send email from your verified domains (phishing risk)' },
|
|
67
|
+
{ re: /\bSK[0-9a-fA-F]{32}\b/, provider: 'twilio', severity: 'high', title: 'Twilio API key SID', impact: 'send SMS/calls billed to your account' },
|
|
68
|
+
{ re: /\bhf_[a-zA-Z0-9]{30,}\b/, provider: 'huggingface', severity: 'high', title: 'HuggingFace token', impact: 'access to your private models and datasets' },
|
|
69
|
+
{ re: /\bnpm_[a-zA-Z0-9]{36,}\b/, provider: 'npm', severity: 'high', title: 'npm access token', impact: 'publish packages as you (supply-chain risk)' },
|
|
70
|
+
{ re: /\bhvs\.[a-zA-Z0-9_-]{20,}\b/, provider: 'vault', severity: 'critical', title: 'HashiCorp Vault token', impact: 'access to every secret the token policy allows' },
|
|
71
|
+
{ re: /\bag_org_[a-zA-Z0-9_-]{20,}\b/, provider: 'fullcourtdefense', severity: 'high', title: 'FullCourtDefense org API key', impact: 'org-scoped scan and shield API access' },
|
|
72
|
+
{ re: /\bshsk_[a-zA-Z0-9]{20,}\b/, provider: 'fullcourtdefense', severity: 'high', title: 'Shield API key', impact: 'submit scans as this shield' },
|
|
73
|
+
{ re: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/, provider: 'jwt', severity: 'medium', title: 'JWT token', impact: 'impersonate the session until the token expires' },
|
|
74
|
+
{ re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/, provider: 'private_key', severity: 'critical', title: 'Private key material', impact: 'authenticate as the key owner' },
|
|
75
|
+
{ re: /postgres(?:ql)?:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'Database connection string', impact: 'direct database read/write access' },
|
|
76
|
+
{ re: /mysql:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'MySQL connection string', impact: 'direct database read/write access' },
|
|
77
|
+
{ re: /rediss?:\/\/[^\s'"]*:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'Redis connection string', impact: 'direct cache/queue access' },
|
|
78
|
+
{ re: /mongodb(?:\+srv)?:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'MongoDB connection string', impact: 'direct database read/write access' },
|
|
63
79
|
];
|
|
80
|
+
/** Provider-specific rotation guidance (falls back to the generic advice). */
|
|
81
|
+
const PROVIDER_REMEDIATION = {
|
|
82
|
+
openai: 'Revoke it at platform.openai.com → API keys, create a new one, and store it in a secret manager or OS keychain — not in plain files.',
|
|
83
|
+
anthropic: 'Revoke it in the Anthropic console → API keys, create a new one, and store it in a secret manager — not in plain files.',
|
|
84
|
+
github: 'Revoke it at github.com → Settings → Developer settings → Tokens, then create a minimally-scoped replacement.',
|
|
85
|
+
gitlab: 'Revoke it in GitLab → Preferences → Access tokens, then create a minimally-scoped replacement.',
|
|
86
|
+
aws: 'Deactivate the key in IAM → Security credentials, rotate, and prefer short-lived credentials (SSO/assume-role) over static keys.',
|
|
87
|
+
google: 'Regenerate the key in Google Cloud console → APIs & Services → Credentials, and add API/IP restrictions to the new key.',
|
|
88
|
+
stripe: 'Roll the key in the Stripe dashboard → Developers → API keys. Live keys should only exist in your deployment platform\u2019s secret store.',
|
|
89
|
+
sendgrid: 'Delete this key in SendGrid → Settings → API Keys and create a scoped replacement. SendGrid keys are shown once — the new one goes straight into a secret manager.',
|
|
90
|
+
slack: 'Regenerate the token/webhook in your Slack app settings (api.slack.com/apps) and update the consumer.',
|
|
91
|
+
npm: 'Revoke it with `npm token revoke` and create a granular automation token with the minimum package scope.',
|
|
92
|
+
vault: 'Revoke the token (`vault token revoke`) and issue short-TTL tokens instead of long-lived ones.',
|
|
93
|
+
database: 'Change the database password and move the connection string to a secret manager; restrict the DB user\u2019s privileges.',
|
|
94
|
+
};
|
|
64
95
|
const SKIP_DIR_NAMES = new Set([
|
|
65
96
|
'node_modules', '.git', 'vendor', 'dist', 'build', '__pycache__', '.venv', 'venv',
|
|
66
97
|
'Library', 'Caches', 'Cache', 'Temp', 'tmp', 'Windows', 'Program Files', 'Program Files (x86)',
|
|
@@ -93,33 +124,67 @@ function scoreFromFindings(findings) {
|
|
|
93
124
|
return Math.max(0, Math.min(100, score));
|
|
94
125
|
}
|
|
95
126
|
function scanTextLines(filePath, content, category, out, seen) {
|
|
127
|
+
// Placeholder/template files (.env.example, *.sample, *.template) hold intentional
|
|
128
|
+
// dummy values — report as info so real findings aren't drowned out (Gitleaks
|
|
129
|
+
// ignores these entirely; we keep them visible but non-alarming).
|
|
130
|
+
const isExampleFile = /\.(example|sample|template|dist)(\.|$)/i.test(path.basename(filePath));
|
|
131
|
+
// Vars with client-bundle prefixes ship to the browser by design.
|
|
132
|
+
const clientExposedPrefix = /^\s*(?:VITE_|NEXT_PUBLIC_|REACT_APP_|EXPO_PUBLIC_|NUXT_PUBLIC_)/;
|
|
96
133
|
const lines = content.split(/\r?\n/);
|
|
97
134
|
for (let i = 0; i < lines.length; i++) {
|
|
98
135
|
const line = lines[i];
|
|
99
136
|
if (!line.trim() || line.trim().startsWith('#'))
|
|
100
137
|
continue;
|
|
138
|
+
// First (most specific) pattern wins per matched string — prevents e.g. an
|
|
139
|
+
// sk-ant- Anthropic key from also being reported as a generic OpenAI sk- key.
|
|
140
|
+
const matchedRaws = [];
|
|
101
141
|
for (const pattern of SECRET_PATTERNS) {
|
|
102
142
|
const match = line.match(pattern.re);
|
|
103
143
|
if (!match)
|
|
104
144
|
continue;
|
|
105
145
|
const raw = match[0];
|
|
146
|
+
if (matchedRaws.some(prev => prev.includes(raw) || raw.includes(prev)))
|
|
147
|
+
continue;
|
|
148
|
+
matchedRaws.push(raw);
|
|
106
149
|
const id = findingId([filePath, String(i + 1), pattern.provider, raw.slice(0, 12)]);
|
|
107
150
|
if (seen.has(id))
|
|
108
151
|
continue;
|
|
109
152
|
seen.add(id);
|
|
153
|
+
// Firebase/Google web API keys behind a client-bundle prefix are public by
|
|
154
|
+
// design — the risk is a missing restriction, not the exposure itself.
|
|
155
|
+
const isClientExposedWebKey = pattern.provider === 'google' && clientExposedPrefix.test(line);
|
|
156
|
+
let severity = pattern.severity;
|
|
157
|
+
let title = pattern.title;
|
|
158
|
+
let detail = pattern.impact
|
|
159
|
+
? `Matched ${pattern.provider} pattern in ${path.basename(filePath)}. If leaked: ${pattern.impact}.`
|
|
160
|
+
: `Matched ${pattern.provider} pattern in ${path.basename(filePath)}`;
|
|
161
|
+
let remediation = pattern.provider === 'private_key'
|
|
162
|
+
? 'Move keys to a secure store; never commit private keys. Use ssh-agent or encrypted keys.'
|
|
163
|
+
: PROVIDER_REMEDIATION[pattern.provider]
|
|
164
|
+
|| 'Rotate this credential immediately and store it in a secret manager or OS keychain — not in plain files.';
|
|
165
|
+
if (isExampleFile) {
|
|
166
|
+
severity = 'info';
|
|
167
|
+
title = `${pattern.title} (example/template file)`;
|
|
168
|
+
detail = `Matched ${pattern.provider} pattern in ${path.basename(filePath)} — looks like an intentional placeholder.`;
|
|
169
|
+
remediation = 'Confirm this is a dummy value. If a real credential was pasted into the example file, rotate it and replace with a placeholder.';
|
|
170
|
+
}
|
|
171
|
+
else if (isClientExposedWebKey) {
|
|
172
|
+
severity = 'medium';
|
|
173
|
+
title = 'Google/Firebase web API key (client-exposed)';
|
|
174
|
+
detail = `Matched google pattern in ${path.basename(filePath)} on a client-bundle variable — this key ships to the browser by design.`;
|
|
175
|
+
remediation = 'Not a secret in the classic sense, but add HTTP-referrer/API restrictions in Google Cloud console → Credentials so it only works from your domains.';
|
|
176
|
+
}
|
|
110
177
|
out.push({
|
|
111
178
|
id,
|
|
112
179
|
category,
|
|
113
|
-
severity
|
|
180
|
+
severity,
|
|
114
181
|
provider: pattern.provider,
|
|
115
182
|
filePath,
|
|
116
183
|
lineNumber: i + 1,
|
|
117
|
-
title
|
|
118
|
-
detail
|
|
184
|
+
title,
|
|
185
|
+
detail,
|
|
119
186
|
redactedPreview: redact(raw),
|
|
120
|
-
remediation
|
|
121
|
-
? 'Move keys to a secure store; never commit private keys. Use ssh-agent or encrypted keys.'
|
|
122
|
-
: 'Rotate this credential immediately and store it in a secret manager or OS keychain — not in plain files.',
|
|
187
|
+
remediation,
|
|
123
188
|
});
|
|
124
189
|
if (out.length >= MAX_FINDINGS)
|
|
125
190
|
return;
|
package/dist/version.json
CHANGED