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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. 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;