getdoorman 1.0.0
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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doorman CLI - Secret Detection Rules
|
|
3
|
+
* SEC-SEC-001 through SEC-SEC-025
|
|
4
|
+
*
|
|
5
|
+
* Detects hardcoded secrets, API keys, tokens, and credentials
|
|
6
|
+
* across source files, configs, and CI pipelines.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
10
|
+
const isJS = (f) => JS_EXT.some((ext) => f.endsWith(ext));
|
|
11
|
+
const isConfig = (f) =>
|
|
12
|
+
f.endsWith('.json') ||
|
|
13
|
+
f.endsWith('.yml') ||
|
|
14
|
+
f.endsWith('.yaml') ||
|
|
15
|
+
f.endsWith('.toml');
|
|
16
|
+
const isTestFile = (f) =>
|
|
17
|
+
f.includes('test') ||
|
|
18
|
+
f.includes('spec') ||
|
|
19
|
+
f.includes('mock') ||
|
|
20
|
+
f.includes('fixture') ||
|
|
21
|
+
f.includes('__tests__');
|
|
22
|
+
|
|
23
|
+
// Lines to skip: comments, process.env refs, os.environ refs
|
|
24
|
+
function shouldSkipLine(line) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (
|
|
27
|
+
trimmed.startsWith('//') ||
|
|
28
|
+
trimmed.startsWith('#') ||
|
|
29
|
+
trimmed.startsWith('*') ||
|
|
30
|
+
trimmed.startsWith('/*')
|
|
31
|
+
) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (/process\.env/i.test(line) || /os\.environ/i.test(line)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Files to skip for secret scanning
|
|
41
|
+
function shouldSkipFile(filePath) {
|
|
42
|
+
const base = filePath.split('/').pop();
|
|
43
|
+
if (base.startsWith('.env')) return true;
|
|
44
|
+
if (isTestFile(filePath)) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scanLines(filePath, content, regex, makeMessage, confidence) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
if (shouldSkipFile(filePath)) return findings;
|
|
51
|
+
if (!content) return findings;
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i];
|
|
55
|
+
if (shouldSkipLine(line)) continue;
|
|
56
|
+
if (regex.test(line)) {
|
|
57
|
+
findings.push({
|
|
58
|
+
file: filePath,
|
|
59
|
+
line: i + 1,
|
|
60
|
+
message: typeof makeMessage === 'function' ? makeMessage(line) : makeMessage,
|
|
61
|
+
confidence,
|
|
62
|
+
snippet: line.trim().substring(0, 120),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return findings;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rules = [
|
|
70
|
+
// SEC-SEC-001: AWS Access Key
|
|
71
|
+
{
|
|
72
|
+
id: 'SEC-SEC-001',
|
|
73
|
+
category: 'security',
|
|
74
|
+
severity: 'critical',
|
|
75
|
+
confidence: 'definite',
|
|
76
|
+
title: 'AWS access key detected',
|
|
77
|
+
check({ files }) {
|
|
78
|
+
const findings = [];
|
|
79
|
+
const pattern = /AKIA[0-9A-Z]{16}/;
|
|
80
|
+
for (const [filePath, content] of files) {
|
|
81
|
+
findings.push(
|
|
82
|
+
...scanLines(filePath, content, pattern, 'AWS access key ID found in source code. Use environment variables or a secrets manager instead.', 'definite')
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// SEC-SEC-002: AWS Secret Key
|
|
90
|
+
{
|
|
91
|
+
id: 'SEC-SEC-002',
|
|
92
|
+
category: 'security',
|
|
93
|
+
severity: 'critical',
|
|
94
|
+
confidence: 'definite',
|
|
95
|
+
title: 'AWS secret key detected',
|
|
96
|
+
check({ files }) {
|
|
97
|
+
const findings = [];
|
|
98
|
+
const pattern = /(?:aws)?_?secret_?(?:access)?_?key.*[:=]\s*['"][A-Za-z0-9/+=]{40}['"]/i;
|
|
99
|
+
for (const [filePath, content] of files) {
|
|
100
|
+
findings.push(
|
|
101
|
+
...scanLines(filePath, content, pattern, 'AWS secret access key found. Rotate this key immediately and use a secrets manager.', 'definite')
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// SEC-SEC-003: Generic API key / high-entropy secret
|
|
109
|
+
{
|
|
110
|
+
id: 'SEC-SEC-003',
|
|
111
|
+
category: 'security',
|
|
112
|
+
severity: 'high',
|
|
113
|
+
confidence: 'likely',
|
|
114
|
+
title: 'Generic API key or secret detected',
|
|
115
|
+
check({ files }) {
|
|
116
|
+
const findings = [];
|
|
117
|
+
const pattern = /(?:api[_-]?key|secret|token|password|passwd|auth[_-]?key|access[_-]?key)\s*[:=]\s*['"][A-Za-z0-9/+=_\-]{16,}['"]/i;
|
|
118
|
+
for (const [filePath, content] of files) {
|
|
119
|
+
if (shouldSkipFile(filePath)) continue;
|
|
120
|
+
if (!isJS(filePath) && !isConfig(filePath)) continue;
|
|
121
|
+
if (!content) continue;
|
|
122
|
+
const lines = content.split('\n');
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (shouldSkipLine(line)) continue;
|
|
126
|
+
if (pattern.test(line)) {
|
|
127
|
+
// Exclude obvious placeholders
|
|
128
|
+
if (/['"](?:your[_-]|example|placeholder|changeme|xxx|TODO|REPLACE)/i.test(line)) continue;
|
|
129
|
+
findings.push({
|
|
130
|
+
file: filePath,
|
|
131
|
+
line: i + 1,
|
|
132
|
+
message: 'Potential hardcoded secret assigned to a sensitive variable name. Move to environment variables.',
|
|
133
|
+
confidence: 'likely',
|
|
134
|
+
snippet: line.trim().substring(0, 120),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return findings;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// SEC-SEC-004: Database connection string with password
|
|
144
|
+
{
|
|
145
|
+
id: 'SEC-SEC-004',
|
|
146
|
+
category: 'security',
|
|
147
|
+
severity: 'critical',
|
|
148
|
+
confidence: 'definite',
|
|
149
|
+
title: 'Database connection string with embedded password',
|
|
150
|
+
check({ files }) {
|
|
151
|
+
const findings = [];
|
|
152
|
+
const pattern = /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/;
|
|
153
|
+
for (const [filePath, content] of files) {
|
|
154
|
+
findings.push(
|
|
155
|
+
...scanLines(filePath, content, pattern, 'Database connection string with embedded credentials. Use environment variables for connection strings.', 'definite')
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return findings;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// SEC-SEC-005: Private key file in repo
|
|
163
|
+
{
|
|
164
|
+
id: 'SEC-SEC-005',
|
|
165
|
+
category: 'security',
|
|
166
|
+
severity: 'critical',
|
|
167
|
+
confidence: 'definite',
|
|
168
|
+
title: 'Private key file detected in repository',
|
|
169
|
+
check({ files }) {
|
|
170
|
+
const findings = [];
|
|
171
|
+
const keyExtensions = ['.pem', '.key', '.p12', '.pfx', '.jks'];
|
|
172
|
+
for (const [filePath, content] of files) {
|
|
173
|
+
if (isTestFile(filePath)) continue;
|
|
174
|
+
const lower = filePath.toLowerCase();
|
|
175
|
+
if (keyExtensions.some((ext) => lower.endsWith(ext))) {
|
|
176
|
+
findings.push({
|
|
177
|
+
file: filePath,
|
|
178
|
+
line: 1,
|
|
179
|
+
message: 'Private key file detected in repository. Remove it, add to .gitignore, and rotate the key.',
|
|
180
|
+
confidence: 'definite',
|
|
181
|
+
snippet: filePath,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// SEC-SEC-006: .env file committed
|
|
190
|
+
{
|
|
191
|
+
id: 'SEC-SEC-006',
|
|
192
|
+
category: 'security',
|
|
193
|
+
severity: 'high',
|
|
194
|
+
confidence: 'likely',
|
|
195
|
+
title: '.env file committed to repository',
|
|
196
|
+
check({ files }) {
|
|
197
|
+
const findings = [];
|
|
198
|
+
for (const [filePath, content] of files) {
|
|
199
|
+
const base = filePath.split('/').pop();
|
|
200
|
+
if (base === '.env') {
|
|
201
|
+
findings.push({
|
|
202
|
+
file: filePath,
|
|
203
|
+
line: 1,
|
|
204
|
+
message: '.env file is committed to the repository. Remove it from version control and add .env to .gitignore.',
|
|
205
|
+
confidence: 'definite',
|
|
206
|
+
autofix: 'echo ".env" >> .gitignore && git rm --cached .env',
|
|
207
|
+
snippet: filePath,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// SEC-SEC-007: .env not in .gitignore
|
|
216
|
+
{
|
|
217
|
+
id: 'SEC-SEC-007',
|
|
218
|
+
category: 'security',
|
|
219
|
+
severity: 'high',
|
|
220
|
+
confidence: 'likely',
|
|
221
|
+
title: '.env not listed in .gitignore',
|
|
222
|
+
check({ files }) {
|
|
223
|
+
const findings = [];
|
|
224
|
+
let gitignorePath = null;
|
|
225
|
+
let gitignoreContent = null;
|
|
226
|
+
for (const [filePath, content] of files) {
|
|
227
|
+
if (filePath.endsWith('.gitignore') && !filePath.includes('node_modules')) {
|
|
228
|
+
gitignorePath = filePath;
|
|
229
|
+
gitignoreContent = content;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!gitignorePath || !gitignoreContent) {
|
|
234
|
+
findings.push({
|
|
235
|
+
file: '.gitignore',
|
|
236
|
+
line: 1,
|
|
237
|
+
message: 'No .gitignore found or .env is not listed. Ensure .env is in .gitignore to prevent accidental commits.',
|
|
238
|
+
confidence: 'suggestion',
|
|
239
|
+
snippet: '.gitignore missing or empty',
|
|
240
|
+
});
|
|
241
|
+
return findings;
|
|
242
|
+
}
|
|
243
|
+
const lines = gitignoreContent.split('\n').map((l) => l.trim());
|
|
244
|
+
const hasEnv = lines.some(
|
|
245
|
+
(l) => l === '.env' || l === '.env*' || l === '.env.*' || l === '*.env'
|
|
246
|
+
);
|
|
247
|
+
if (!hasEnv) {
|
|
248
|
+
findings.push({
|
|
249
|
+
file: gitignorePath,
|
|
250
|
+
line: 1,
|
|
251
|
+
message: '.env is not listed in .gitignore. Add .env to prevent accidental commits of secrets.',
|
|
252
|
+
confidence: 'suggestion',
|
|
253
|
+
snippet: 'Missing .env entry in .gitignore',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
// SEC-SEC-008: Stripe secret key
|
|
261
|
+
{
|
|
262
|
+
id: 'SEC-SEC-008',
|
|
263
|
+
category: 'security',
|
|
264
|
+
severity: 'critical',
|
|
265
|
+
confidence: 'definite',
|
|
266
|
+
title: 'Stripe secret key detected',
|
|
267
|
+
check({ files }) {
|
|
268
|
+
const findings = [];
|
|
269
|
+
const pattern = /sk_(?:live|test)_[a-zA-Z0-9]{20,}/;
|
|
270
|
+
for (const [filePath, content] of files) {
|
|
271
|
+
findings.push(
|
|
272
|
+
...scanLines(filePath, content, pattern, 'Stripe secret key found. Rotate this key and use environment variables.', 'definite')
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return findings;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// SEC-SEC-009: GitHub token
|
|
280
|
+
{
|
|
281
|
+
id: 'SEC-SEC-009',
|
|
282
|
+
category: 'security',
|
|
283
|
+
severity: 'critical',
|
|
284
|
+
confidence: 'definite',
|
|
285
|
+
title: 'GitHub personal access token detected',
|
|
286
|
+
check({ files }) {
|
|
287
|
+
const findings = [];
|
|
288
|
+
const pattern = /(?:ghp|github_pat)_[a-zA-Z0-9]{20,}/;
|
|
289
|
+
for (const [filePath, content] of files) {
|
|
290
|
+
findings.push(
|
|
291
|
+
...scanLines(filePath, content, pattern, 'GitHub token found in source code. Revoke and regenerate, then store in a secrets manager.', 'definite')
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return findings;
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// SEC-SEC-010: Google API key
|
|
299
|
+
{
|
|
300
|
+
id: 'SEC-SEC-010',
|
|
301
|
+
category: 'security',
|
|
302
|
+
severity: 'high',
|
|
303
|
+
confidence: 'likely',
|
|
304
|
+
title: 'Google API key detected',
|
|
305
|
+
check({ files }) {
|
|
306
|
+
const findings = [];
|
|
307
|
+
const pattern = /AIza[0-9A-Za-z_-]{35}/;
|
|
308
|
+
for (const [filePath, content] of files) {
|
|
309
|
+
findings.push(
|
|
310
|
+
...scanLines(filePath, content, pattern, 'Google API key found. Restrict the key in Google Cloud Console and move to environment variables.', 'definite')
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return findings;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// SEC-SEC-011: Slack webhook URL
|
|
318
|
+
{
|
|
319
|
+
id: 'SEC-SEC-011',
|
|
320
|
+
category: 'security',
|
|
321
|
+
severity: 'high',
|
|
322
|
+
confidence: 'likely',
|
|
323
|
+
title: 'Slack webhook URL detected',
|
|
324
|
+
check({ files }) {
|
|
325
|
+
const findings = [];
|
|
326
|
+
const pattern = /hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/;
|
|
327
|
+
for (const [filePath, content] of files) {
|
|
328
|
+
findings.push(
|
|
329
|
+
...scanLines(filePath, content, pattern, 'Slack webhook URL found. Store webhook URLs in environment variables to prevent abuse.', 'definite')
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return findings;
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// SEC-SEC-012: SendGrid / Mailgun key
|
|
337
|
+
{
|
|
338
|
+
id: 'SEC-SEC-012',
|
|
339
|
+
category: 'security',
|
|
340
|
+
severity: 'high',
|
|
341
|
+
confidence: 'likely',
|
|
342
|
+
title: 'SendGrid or Mailgun API key detected',
|
|
343
|
+
check({ files }) {
|
|
344
|
+
const findings = [];
|
|
345
|
+
const sendgridPattern = /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/;
|
|
346
|
+
const mailgunPattern = /key-[a-f0-9]{32}/;
|
|
347
|
+
for (const [filePath, content] of files) {
|
|
348
|
+
findings.push(
|
|
349
|
+
...scanLines(filePath, content, sendgridPattern, 'SendGrid API key found. Rotate and move to environment variables.', 'definite')
|
|
350
|
+
);
|
|
351
|
+
findings.push(
|
|
352
|
+
...scanLines(filePath, content, mailgunPattern, 'Mailgun API key found. Rotate and move to environment variables.', 'definite')
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return findings;
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// SEC-SEC-013: Firebase config without security rules
|
|
360
|
+
{
|
|
361
|
+
id: 'SEC-SEC-013',
|
|
362
|
+
category: 'security',
|
|
363
|
+
severity: 'medium',
|
|
364
|
+
confidence: 'likely',
|
|
365
|
+
title: 'Firebase config without security rules file',
|
|
366
|
+
check({ files }) {
|
|
367
|
+
const findings = [];
|
|
368
|
+
let hasFirebaseConfig = false;
|
|
369
|
+
let firebaseConfigPath = 'unknown';
|
|
370
|
+
let hasRulesFile = false;
|
|
371
|
+
for (const [filePath, content] of files) {
|
|
372
|
+
if (content && /firebaseConfig\s*=/.test(content) && /apiKey/.test(content)) {
|
|
373
|
+
hasFirebaseConfig = true;
|
|
374
|
+
firebaseConfigPath = filePath;
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
filePath.includes('firestore.rules') ||
|
|
378
|
+
filePath.includes('database.rules') ||
|
|
379
|
+
filePath.includes('storage.rules') ||
|
|
380
|
+
filePath.endsWith('.rules')
|
|
381
|
+
) {
|
|
382
|
+
hasRulesFile = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!hasFirebaseConfig) return findings;
|
|
386
|
+
if (!hasRulesFile) {
|
|
387
|
+
findings.push({
|
|
388
|
+
file: firebaseConfigPath,
|
|
389
|
+
line: 1,
|
|
390
|
+
message: 'Firebase config found but no security rules file detected. Add Firestore/Realtime Database security rules.',
|
|
391
|
+
confidence: 'suggestion',
|
|
392
|
+
snippet: 'firebaseConfig present without .rules file',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return findings;
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
// SEC-SEC-014: OpenAI API key
|
|
400
|
+
{
|
|
401
|
+
id: 'SEC-SEC-014',
|
|
402
|
+
category: 'security',
|
|
403
|
+
severity: 'critical',
|
|
404
|
+
confidence: 'definite',
|
|
405
|
+
title: 'OpenAI API key detected',
|
|
406
|
+
check({ files }) {
|
|
407
|
+
const findings = [];
|
|
408
|
+
// Match sk- followed by 20+ alphanumeric, but exclude sk_live/sk_test (Stripe) and sk-ant- (Anthropic)
|
|
409
|
+
const pattern = /sk-(?!ant-|live_|test_)[a-zA-Z0-9]{20,}/;
|
|
410
|
+
for (const [filePath, content] of files) {
|
|
411
|
+
findings.push(
|
|
412
|
+
...scanLines(filePath, content, pattern, 'OpenAI API key found. Rotate this key and store in environment variables.', 'definite')
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return findings;
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
// SEC-SEC-015: Twilio credentials
|
|
420
|
+
{
|
|
421
|
+
id: 'SEC-SEC-015',
|
|
422
|
+
category: 'security',
|
|
423
|
+
severity: 'high',
|
|
424
|
+
confidence: 'likely',
|
|
425
|
+
title: 'Twilio credentials detected',
|
|
426
|
+
check({ files }) {
|
|
427
|
+
const findings = [];
|
|
428
|
+
const pattern = /(?:AC|SK)[a-f0-9]{32}/;
|
|
429
|
+
for (const [filePath, content] of files) {
|
|
430
|
+
findings.push(
|
|
431
|
+
...scanLines(filePath, content, pattern, 'Twilio Account SID or API key found. Use environment variables for Twilio credentials.', 'definite')
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return findings;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
// SEC-SEC-016: SSH private key
|
|
439
|
+
{
|
|
440
|
+
id: 'SEC-SEC-016',
|
|
441
|
+
category: 'security',
|
|
442
|
+
severity: 'critical',
|
|
443
|
+
confidence: 'definite',
|
|
444
|
+
title: 'SSH private key detected',
|
|
445
|
+
check({ files }) {
|
|
446
|
+
const findings = [];
|
|
447
|
+
const pattern = /-----BEGIN (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----/;
|
|
448
|
+
for (const [filePath, content] of files) {
|
|
449
|
+
findings.push(
|
|
450
|
+
...scanLines(filePath, content, pattern, 'SSH private key found in source code. Remove immediately, revoke the key, and generate a new one.', 'definite')
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return findings;
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
// SEC-SEC-017: Hardcoded encryption key
|
|
458
|
+
{
|
|
459
|
+
id: 'SEC-SEC-017',
|
|
460
|
+
category: 'security',
|
|
461
|
+
severity: 'critical',
|
|
462
|
+
confidence: 'definite',
|
|
463
|
+
title: 'Hardcoded encryption key detected',
|
|
464
|
+
check({ files }) {
|
|
465
|
+
const findings = [];
|
|
466
|
+
const cipherPattern = /crypto\.(?:createCipher|createCipheriv)\s*\(/;
|
|
467
|
+
for (const [filePath, content] of files) {
|
|
468
|
+
if (shouldSkipFile(filePath)) continue;
|
|
469
|
+
if (!isJS(filePath)) continue;
|
|
470
|
+
if (!content) continue;
|
|
471
|
+
const lines = content.split('\n');
|
|
472
|
+
for (let i = 0; i < lines.length; i++) {
|
|
473
|
+
const line = lines[i];
|
|
474
|
+
if (shouldSkipLine(line)) continue;
|
|
475
|
+
if (cipherPattern.test(line)) {
|
|
476
|
+
// Check if the key argument is a string literal rather than a variable from env
|
|
477
|
+
if (/createCipher(?:iv)?\s*\(\s*['"][^'"]+['"]\s*,\s*['"]/.test(line)) {
|
|
478
|
+
findings.push({
|
|
479
|
+
file: filePath,
|
|
480
|
+
line: i + 1,
|
|
481
|
+
message: 'Encryption key is hardcoded as a string literal. Derive keys from a KMS or environment variable.',
|
|
482
|
+
confidence: 'definite',
|
|
483
|
+
snippet: line.trim().substring(0, 120),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return findings;
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
// SEC-SEC-018: .env.local / .env.production committed
|
|
494
|
+
{
|
|
495
|
+
id: 'SEC-SEC-018',
|
|
496
|
+
category: 'security',
|
|
497
|
+
severity: 'high',
|
|
498
|
+
confidence: 'likely',
|
|
499
|
+
title: 'Environment-specific .env file committed',
|
|
500
|
+
check({ files }) {
|
|
501
|
+
const findings = [];
|
|
502
|
+
const envFiles = ['.env.local', '.env.production', '.env.staging', '.env.development'];
|
|
503
|
+
for (const [filePath, content] of files) {
|
|
504
|
+
const base = filePath.split('/').pop();
|
|
505
|
+
if (envFiles.includes(base)) {
|
|
506
|
+
findings.push({
|
|
507
|
+
file: filePath,
|
|
508
|
+
line: 1,
|
|
509
|
+
message: `${base} is committed to the repository. Remove from version control and add to .gitignore.`,
|
|
510
|
+
confidence: 'definite',
|
|
511
|
+
snippet: filePath,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return findings;
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
// SEC-SEC-019: Secrets in CI config
|
|
520
|
+
{
|
|
521
|
+
id: 'SEC-SEC-019',
|
|
522
|
+
category: 'security',
|
|
523
|
+
severity: 'critical',
|
|
524
|
+
confidence: 'definite',
|
|
525
|
+
title: 'Hardcoded secrets in CI/CD configuration',
|
|
526
|
+
check({ files }) {
|
|
527
|
+
const findings = [];
|
|
528
|
+
const sensitiveVarPattern = /(?:password|secret|token|api_key|auth|credentials)\s*[:=]\s*(?!.*\$\{\{\s*secrets\.)/i;
|
|
529
|
+
for (const [filePath, content] of files) {
|
|
530
|
+
if (
|
|
531
|
+
!filePath.includes('.github/workflows/') ||
|
|
532
|
+
(!filePath.endsWith('.yml') && !filePath.endsWith('.yaml'))
|
|
533
|
+
) continue;
|
|
534
|
+
if (!content) continue;
|
|
535
|
+
const lines = content.split('\n');
|
|
536
|
+
for (let i = 0; i < lines.length; i++) {
|
|
537
|
+
const line = lines[i];
|
|
538
|
+
if (shouldSkipLine(line)) continue;
|
|
539
|
+
if (sensitiveVarPattern.test(line)) {
|
|
540
|
+
if (/\$\{\{\s*secrets\./.test(line)) continue;
|
|
541
|
+
if (/[:=]\s*['"]?\s*['"]?\s*$/.test(line)) continue;
|
|
542
|
+
findings.push({
|
|
543
|
+
file: filePath,
|
|
544
|
+
line: i + 1,
|
|
545
|
+
message: 'Potential hardcoded secret in CI config. Use GitHub Actions secrets (${{ secrets.NAME }}) instead.',
|
|
546
|
+
confidence: 'likely',
|
|
547
|
+
snippet: line.trim().substring(0, 120),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return findings;
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
// SEC-SEC-020: Sensitive data in error messages
|
|
557
|
+
{
|
|
558
|
+
id: 'SEC-SEC-020',
|
|
559
|
+
category: 'security',
|
|
560
|
+
severity: 'medium',
|
|
561
|
+
confidence: 'likely',
|
|
562
|
+
title: 'Sensitive error data exposed in response',
|
|
563
|
+
check({ files }) {
|
|
564
|
+
const findings = [];
|
|
565
|
+
const responsePattern = /res\.(?:json|send|status\(\d+\)\.(?:json|send))\s*\(/;
|
|
566
|
+
// err.stack leaks internals (file paths, line numbers); err.message is often intentionally surfaced
|
|
567
|
+
const sensitivePattern = /err(?:or)?\.stack\b/;
|
|
568
|
+
for (const [filePath, content] of files) {
|
|
569
|
+
if (shouldSkipFile(filePath)) continue;
|
|
570
|
+
if (!isJS(filePath)) continue;
|
|
571
|
+
if (!content) continue;
|
|
572
|
+
const lines = content.split('\n');
|
|
573
|
+
for (let i = 0; i < lines.length; i++) {
|
|
574
|
+
const line = lines[i];
|
|
575
|
+
if (shouldSkipLine(line)) continue;
|
|
576
|
+
if (responsePattern.test(line) && sensitivePattern.test(line)) {
|
|
577
|
+
findings.push({
|
|
578
|
+
file: filePath,
|
|
579
|
+
line: i + 1,
|
|
580
|
+
message: 'Error details (stack trace or message) sent in HTTP response. Use generic error messages in production.',
|
|
581
|
+
confidence: 'likely',
|
|
582
|
+
snippet: line.trim().substring(0, 120),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return findings;
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// SEC-SEC-021: Sensitive data logged
|
|
592
|
+
{
|
|
593
|
+
id: 'SEC-SEC-021',
|
|
594
|
+
category: 'security',
|
|
595
|
+
severity: 'medium',
|
|
596
|
+
confidence: 'likely',
|
|
597
|
+
title: 'Sensitive data written to logs',
|
|
598
|
+
check({ files }) {
|
|
599
|
+
const findings = [];
|
|
600
|
+
const logPattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:log|info|warn|error|debug))\s*\(/;
|
|
601
|
+
const sensitivePattern = /(?:password|passwd|token|secret|creditcard|credit_card|ssn|social_security)/i;
|
|
602
|
+
for (const [filePath, content] of files) {
|
|
603
|
+
if (shouldSkipFile(filePath)) continue;
|
|
604
|
+
if (!isJS(filePath)) continue;
|
|
605
|
+
if (!content) continue;
|
|
606
|
+
const lines = content.split('\n');
|
|
607
|
+
for (let i = 0; i < lines.length; i++) {
|
|
608
|
+
const line = lines[i];
|
|
609
|
+
if (shouldSkipLine(line)) continue;
|
|
610
|
+
if (logPattern.test(line) && sensitivePattern.test(line)) {
|
|
611
|
+
findings.push({
|
|
612
|
+
file: filePath,
|
|
613
|
+
line: i + 1,
|
|
614
|
+
message: 'Sensitive data (password/token/secret/creditcard) is being logged. Remove or redact before logging.',
|
|
615
|
+
confidence: 'likely',
|
|
616
|
+
snippet: line.trim().substring(0, 120),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return findings;
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
// SEC-SEC-022: Docker build with secrets in ARG
|
|
626
|
+
{
|
|
627
|
+
id: 'SEC-SEC-022',
|
|
628
|
+
category: 'security',
|
|
629
|
+
severity: 'high',
|
|
630
|
+
confidence: 'likely',
|
|
631
|
+
title: 'Secrets passed via Docker ARG instruction',
|
|
632
|
+
check({ files }) {
|
|
633
|
+
const findings = [];
|
|
634
|
+
const argPattern = /^\s*ARG\s+(\w*(?:password|secret|token|key|api_key|credentials|auth)\w*)/i;
|
|
635
|
+
for (const [filePath, content] of files) {
|
|
636
|
+
if (!filePath.endsWith('Dockerfile') && !filePath.includes('Dockerfile.')) continue;
|
|
637
|
+
if (!content) continue;
|
|
638
|
+
const lines = content.split('\n');
|
|
639
|
+
for (let i = 0; i < lines.length; i++) {
|
|
640
|
+
const line = lines[i];
|
|
641
|
+
const match = argPattern.exec(line);
|
|
642
|
+
if (match) {
|
|
643
|
+
findings.push({
|
|
644
|
+
file: filePath,
|
|
645
|
+
line: i + 1,
|
|
646
|
+
message: `Docker ARG "${match[1]}" appears to contain a secret. ARG values are visible in image history. Use --secret flag or multi-stage builds.`,
|
|
647
|
+
confidence: 'likely',
|
|
648
|
+
snippet: line.trim().substring(0, 120),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return findings;
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// SEC-SEC-023: Anthropic API key
|
|
658
|
+
{
|
|
659
|
+
id: 'SEC-SEC-023',
|
|
660
|
+
category: 'security',
|
|
661
|
+
severity: 'critical',
|
|
662
|
+
confidence: 'definite',
|
|
663
|
+
title: 'Anthropic API key detected',
|
|
664
|
+
check({ files }) {
|
|
665
|
+
const findings = [];
|
|
666
|
+
const pattern = /sk-ant-[a-zA-Z0-9_-]{20,}/;
|
|
667
|
+
for (const [filePath, content] of files) {
|
|
668
|
+
findings.push(
|
|
669
|
+
...scanLines(filePath, content, pattern, 'Anthropic API key found. Rotate this key and store in environment variables.', 'definite')
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return findings;
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// SEC-SEC-024: Supabase service_role key in client-side code
|
|
677
|
+
{
|
|
678
|
+
id: 'SEC-SEC-024',
|
|
679
|
+
category: 'security',
|
|
680
|
+
severity: 'critical',
|
|
681
|
+
confidence: 'definite',
|
|
682
|
+
title: 'Supabase service_role key in non-server file',
|
|
683
|
+
check({ files }) {
|
|
684
|
+
const findings = [];
|
|
685
|
+
const serviceKeyPattern = /(?:service_role|service_key|serviceRole|serviceKey)\s*[:=]/i;
|
|
686
|
+
const serverPaths = ['/server/', '/api/', '/backend/', '/lib/server/', '/pages/api/', '/app/api/'];
|
|
687
|
+
for (const [filePath, content] of files) {
|
|
688
|
+
if (shouldSkipFile(filePath)) continue;
|
|
689
|
+
if (!isJS(filePath)) continue;
|
|
690
|
+
if (!content) continue;
|
|
691
|
+
// Only flag if NOT in a server-side path
|
|
692
|
+
const isServerFile = serverPaths.some((p) => filePath.includes(p));
|
|
693
|
+
if (isServerFile) continue;
|
|
694
|
+
const lines = content.split('\n');
|
|
695
|
+
for (let i = 0; i < lines.length; i++) {
|
|
696
|
+
const line = lines[i];
|
|
697
|
+
if (shouldSkipLine(line)) continue;
|
|
698
|
+
if (serviceKeyPattern.test(line)) {
|
|
699
|
+
findings.push({
|
|
700
|
+
file: filePath,
|
|
701
|
+
line: i + 1,
|
|
702
|
+
message: 'Supabase service_role key referenced in a non-server file. This key bypasses RLS and must only be used server-side.',
|
|
703
|
+
confidence: 'likely',
|
|
704
|
+
snippet: line.trim().substring(0, 120),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return findings;
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
// SEC-SEC-025: Hardcoded private/internal IPs
|
|
714
|
+
{
|
|
715
|
+
id: 'SEC-SEC-025',
|
|
716
|
+
category: 'security',
|
|
717
|
+
severity: 'low',
|
|
718
|
+
confidence: 'suggestion',
|
|
719
|
+
title: 'Hardcoded internal IP address detected',
|
|
720
|
+
check({ files }) {
|
|
721
|
+
const findings = [];
|
|
722
|
+
const pattern = /\b(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/;
|
|
723
|
+
for (const [filePath, content] of files) {
|
|
724
|
+
if (shouldSkipFile(filePath)) continue;
|
|
725
|
+
if (!isJS(filePath) && !isConfig(filePath)) continue;
|
|
726
|
+
if (!content) continue;
|
|
727
|
+
const lines = content.split('\n');
|
|
728
|
+
for (let i = 0; i < lines.length; i++) {
|
|
729
|
+
const line = lines[i];
|
|
730
|
+
if (shouldSkipLine(line)) continue;
|
|
731
|
+
if (pattern.test(line)) {
|
|
732
|
+
findings.push({
|
|
733
|
+
file: filePath,
|
|
734
|
+
line: i + 1,
|
|
735
|
+
message: 'Hardcoded internal IP address found. Use configuration or DNS-based service discovery instead.',
|
|
736
|
+
confidence: 'likely',
|
|
737
|
+
snippet: line.trim().substring(0, 120),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return findings;
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
];
|
|
746
|
+
|
|
747
|
+
export default rules;
|