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,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doorman CLI - Security Misconfiguration Rules
|
|
3
|
+
* SEC-MISC-001 through SEC-MISC-015
|
|
4
|
+
*
|
|
5
|
+
* Detects insecure defaults, open services, exposed admin panels,
|
|
6
|
+
* debug modes, and permissive access controls.
|
|
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
|
+
function shouldSkipLine(line) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
return (
|
|
26
|
+
trimmed.startsWith('//') ||
|
|
27
|
+
trimmed.startsWith('#') ||
|
|
28
|
+
trimmed.startsWith('*') ||
|
|
29
|
+
trimmed.startsWith('/*')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scanLines(filePath, content, regex, message, confidence) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
if (isTestFile(filePath)) return findings;
|
|
36
|
+
if (!content) return findings;
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
if (shouldSkipLine(line)) continue;
|
|
41
|
+
if (regex.test(line)) {
|
|
42
|
+
findings.push({
|
|
43
|
+
file: filePath,
|
|
44
|
+
line: i + 1,
|
|
45
|
+
message: typeof message === 'function' ? message(line) : message,
|
|
46
|
+
confidence,
|
|
47
|
+
snippet: line.trim().substring(0, 120),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper: convert scanLines calls from old file-object API to new (filePath, content) API
|
|
55
|
+
// (Already done inline below)
|
|
56
|
+
|
|
57
|
+
const rules = [
|
|
58
|
+
// SEC-MISC-001: Public S3 bucket
|
|
59
|
+
{
|
|
60
|
+
id: 'SEC-MISC-001',
|
|
61
|
+
category: 'security',
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
confidence: 'definite',
|
|
64
|
+
title: 'Public S3 bucket configuration detected',
|
|
65
|
+
check({ files }) {
|
|
66
|
+
const findings = [];
|
|
67
|
+
const aclPattern = /ACL\s*:\s*['"]public-read['"]/;
|
|
68
|
+
const blockPattern = /PublicAccessBlock\s*:\s*false/i;
|
|
69
|
+
for (const [filePath, content] of files) {
|
|
70
|
+
findings.push(
|
|
71
|
+
...scanLines(filePath, content, aclPattern, 'S3 bucket configured with public-read ACL. Restrict access unless public hosting is intentional.', 'definite')
|
|
72
|
+
);
|
|
73
|
+
findings.push(
|
|
74
|
+
...scanLines(filePath, content, blockPattern, 'S3 PublicAccessBlock is disabled. Enable it to prevent accidental public exposure.', 'definite')
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return findings;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// SEC-MISC-002: Database open to all interfaces
|
|
82
|
+
{
|
|
83
|
+
id: 'SEC-MISC-002',
|
|
84
|
+
category: 'security',
|
|
85
|
+
severity: 'critical',
|
|
86
|
+
confidence: 'definite',
|
|
87
|
+
title: 'Database bound to all network interfaces',
|
|
88
|
+
check({ files }) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
const hostPattern = /(?:host|bind_address|bindIp)\s*[:=]\s*['"]?0\.0\.0\.0['"]?/i;
|
|
91
|
+
for (const [filePath, content] of files) {
|
|
92
|
+
if (isTestFile(filePath)) continue;
|
|
93
|
+
if (!isJS(filePath) && !isConfig(filePath)) continue;
|
|
94
|
+
if (!content) continue;
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
if (shouldSkipLine(line)) continue;
|
|
99
|
+
if (hostPattern.test(line)) {
|
|
100
|
+
const context = content.substring(
|
|
101
|
+
Math.max(0, content.indexOf(line) - 200),
|
|
102
|
+
content.indexOf(line) + line.length + 200
|
|
103
|
+
);
|
|
104
|
+
if (/(?:postgres|mysql|mongo|redis|database|sequelize|knex|prisma|typeorm)/i.test(context)) {
|
|
105
|
+
findings.push({
|
|
106
|
+
file: filePath,
|
|
107
|
+
line: i + 1,
|
|
108
|
+
message: 'Database is bound to 0.0.0.0 (all interfaces). Bind to 127.0.0.1 or a private network interface.',
|
|
109
|
+
confidence: 'likely',
|
|
110
|
+
snippet: line.trim().substring(0, 120),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return findings;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// SEC-MISC-003: Exposed .git directory via static serving
|
|
121
|
+
{
|
|
122
|
+
id: 'SEC-MISC-003',
|
|
123
|
+
category: 'security',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
confidence: 'likely',
|
|
126
|
+
title: 'Static file serving may expose .git directory',
|
|
127
|
+
check({ files }) {
|
|
128
|
+
const findings = [];
|
|
129
|
+
const staticPattern = /(?:express\.static|serveStatic)\s*\(\s*['"](?:\.\/?\s*|\/?\s*)['"]/;
|
|
130
|
+
for (const [filePath, content] of files) {
|
|
131
|
+
if (isTestFile(filePath)) continue;
|
|
132
|
+
if (!isJS(filePath)) continue;
|
|
133
|
+
if (!content) continue;
|
|
134
|
+
const lines = content.split('\n');
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
const line = lines[i];
|
|
137
|
+
if (shouldSkipLine(line)) continue;
|
|
138
|
+
if (staticPattern.test(line)) {
|
|
139
|
+
const nearby = lines.slice(Math.max(0, i - 3), i + 4).join('\n');
|
|
140
|
+
if (/dotfiles\s*:\s*['"]deny['"]/i.test(nearby)) continue;
|
|
141
|
+
findings.push({
|
|
142
|
+
file: filePath,
|
|
143
|
+
line: i + 1,
|
|
144
|
+
message: 'Static serving of root directory may expose .git folder. Use dotfiles: "deny" or serve from a public/ subdirectory.',
|
|
145
|
+
confidence: 'definite',
|
|
146
|
+
snippet: line.trim().substring(0, 120),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// SEC-MISC-004: Debug mode in production
|
|
156
|
+
{
|
|
157
|
+
id: 'SEC-MISC-004',
|
|
158
|
+
category: 'security',
|
|
159
|
+
severity: 'high',
|
|
160
|
+
confidence: 'likely',
|
|
161
|
+
title: 'Debug mode enabled in production configuration',
|
|
162
|
+
check({ files }) {
|
|
163
|
+
const findings = [];
|
|
164
|
+
const debugPatterns = [
|
|
165
|
+
{ regex: /DEBUG\s*[:=]\s*['"]?(?:true|True|1|yes)['"]?/, msg: 'DEBUG is set to true.' },
|
|
166
|
+
{ regex: /NODE_ENV\s*[:=]\s*['"]development['"]/, msg: 'NODE_ENV is set to development.' },
|
|
167
|
+
{ regex: /debug\s*:\s*true/, msg: 'debug flag is set to true.' },
|
|
168
|
+
];
|
|
169
|
+
for (const [filePath, content] of files) {
|
|
170
|
+
if (isTestFile(filePath)) continue;
|
|
171
|
+
const isProdConfig =
|
|
172
|
+
filePath.includes('prod') ||
|
|
173
|
+
filePath.includes('deploy') ||
|
|
174
|
+
filePath.includes('docker-compose') ||
|
|
175
|
+
filePath.endsWith('Dockerfile') ||
|
|
176
|
+
(filePath.includes('.github/workflows/') && (filePath.endsWith('.yml') || filePath.endsWith('.yaml')));
|
|
177
|
+
if (!isProdConfig) continue;
|
|
178
|
+
if (!content) continue;
|
|
179
|
+
for (const { regex, msg } of debugPatterns) {
|
|
180
|
+
findings.push(
|
|
181
|
+
...scanLines(filePath, content, regex, `${msg} Ensure debug mode is disabled in production configurations.`, 'likely')
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// SEC-MISC-005: GraphQL introspection enabled
|
|
190
|
+
{
|
|
191
|
+
id: 'SEC-MISC-005',
|
|
192
|
+
category: 'security',
|
|
193
|
+
severity: 'medium',
|
|
194
|
+
confidence: 'likely',
|
|
195
|
+
title: 'GraphQL introspection enabled',
|
|
196
|
+
check({ files }) {
|
|
197
|
+
const findings = [];
|
|
198
|
+
const introspectionTrue = /introspection\s*:\s*true/;
|
|
199
|
+
for (const [filePath, content] of files) {
|
|
200
|
+
if (isTestFile(filePath)) continue;
|
|
201
|
+
if (!isJS(filePath)) continue;
|
|
202
|
+
if (!content) continue;
|
|
203
|
+
const lines = content.split('\n');
|
|
204
|
+
for (let i = 0; i < lines.length; i++) {
|
|
205
|
+
const line = lines[i];
|
|
206
|
+
if (shouldSkipLine(line)) continue;
|
|
207
|
+
if (introspectionTrue.test(line)) {
|
|
208
|
+
findings.push({
|
|
209
|
+
file: filePath,
|
|
210
|
+
line: i + 1,
|
|
211
|
+
message: 'GraphQL introspection is explicitly enabled. Disable in production to prevent schema disclosure.',
|
|
212
|
+
confidence: 'likely',
|
|
213
|
+
snippet: line.trim().substring(0, 120),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (/new ApolloServer\s*\(/.test(content) && !/introspection\s*:/.test(content)) {
|
|
218
|
+
findings.push({
|
|
219
|
+
file: filePath,
|
|
220
|
+
line: 1,
|
|
221
|
+
message: 'ApolloServer created without explicit introspection setting. Set introspection: false for production.',
|
|
222
|
+
confidence: 'suggestion',
|
|
223
|
+
snippet: 'ApolloServer missing introspection config',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return findings;
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// SEC-MISC-006: Exposed admin panel without auth
|
|
232
|
+
{
|
|
233
|
+
id: 'SEC-MISC-006',
|
|
234
|
+
category: 'security',
|
|
235
|
+
severity: 'high',
|
|
236
|
+
confidence: 'likely',
|
|
237
|
+
title: 'Admin route without authentication middleware',
|
|
238
|
+
check({ files }) {
|
|
239
|
+
const findings = [];
|
|
240
|
+
const adminRoutePattern = /(?:app|router)\.(?:get|post|put|delete|use)\s*\(\s*['"]\/admin/;
|
|
241
|
+
const authMiddlewareNames = /(?:auth|authenticate|isAuthenticated|requireAuth|ensureAuth|protect|guard|requireLogin|isLoggedIn|verifyToken|checkAuth)/i;
|
|
242
|
+
for (const [filePath, content] of files) {
|
|
243
|
+
if (isTestFile(filePath)) continue;
|
|
244
|
+
if (!isJS(filePath)) continue;
|
|
245
|
+
if (!content) continue;
|
|
246
|
+
const lines = content.split('\n');
|
|
247
|
+
for (let i = 0; i < lines.length; i++) {
|
|
248
|
+
const line = lines[i];
|
|
249
|
+
if (shouldSkipLine(line)) continue;
|
|
250
|
+
if (adminRoutePattern.test(line)) {
|
|
251
|
+
if (!authMiddlewareNames.test(line)) {
|
|
252
|
+
findings.push({
|
|
253
|
+
file: filePath,
|
|
254
|
+
line: i + 1,
|
|
255
|
+
message: 'Admin route registered without visible auth middleware. Protect admin endpoints with authentication and authorization.',
|
|
256
|
+
confidence: 'likely',
|
|
257
|
+
snippet: line.trim().substring(0, 120),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// SEC-MISC-007: Open Firebase / Supabase rules
|
|
268
|
+
{
|
|
269
|
+
id: 'SEC-MISC-007',
|
|
270
|
+
category: 'security',
|
|
271
|
+
severity: 'critical',
|
|
272
|
+
confidence: 'definite',
|
|
273
|
+
title: 'Permissive database security rules',
|
|
274
|
+
check({ files }) {
|
|
275
|
+
const findings = [];
|
|
276
|
+
const openFirebase = /allow\s+read\s*,\s*write\s*:\s*if\s+true/;
|
|
277
|
+
const openFirebaseAlt = /["']\.read['"]\s*:\s*['"]?true['"]?/;
|
|
278
|
+
for (const [filePath, content] of files) {
|
|
279
|
+
if (isTestFile(filePath)) continue;
|
|
280
|
+
if (!content) continue;
|
|
281
|
+
if (
|
|
282
|
+
filePath.includes('.rules') ||
|
|
283
|
+
filePath.includes('firestore') ||
|
|
284
|
+
filePath.includes('database') ||
|
|
285
|
+
filePath.includes('storage.rules')
|
|
286
|
+
) {
|
|
287
|
+
findings.push(
|
|
288
|
+
...scanLines(
|
|
289
|
+
filePath, content,
|
|
290
|
+
openFirebase,
|
|
291
|
+
'Firebase/Firestore rules allow unrestricted read and write. Implement proper access control rules.',
|
|
292
|
+
'definite'
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
findings.push(
|
|
296
|
+
...scanLines(
|
|
297
|
+
filePath, content,
|
|
298
|
+
openFirebaseAlt,
|
|
299
|
+
'Realtime Database rules are fully open. Restrict access based on authentication.',
|
|
300
|
+
'definite'
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (filePath.endsWith('.sql') || filePath.includes('migration')) {
|
|
305
|
+
const rlsDisabledPattern = /ALTER\s+TABLE\s+\S+\s+DISABLE\s+ROW\s+LEVEL\s+SECURITY/i;
|
|
306
|
+
findings.push(
|
|
307
|
+
...scanLines(
|
|
308
|
+
filePath, content,
|
|
309
|
+
rlsDisabledPattern,
|
|
310
|
+
'Row Level Security is disabled on a table. Enable RLS and add appropriate policies.',
|
|
311
|
+
'definite'
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return findings;
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// SEC-MISC-008: Directory listing enabled
|
|
321
|
+
{
|
|
322
|
+
id: 'SEC-MISC-008',
|
|
323
|
+
category: 'security',
|
|
324
|
+
severity: 'medium',
|
|
325
|
+
confidence: 'likely',
|
|
326
|
+
title: 'Directory listing enabled',
|
|
327
|
+
check({ files }) {
|
|
328
|
+
const findings = [];
|
|
329
|
+
const autoIndexPattern = /autoIndex\s*:\s*true/;
|
|
330
|
+
const apachePattern = /Options\s+\+?Indexes/;
|
|
331
|
+
const nginxPattern = /autoindex\s+on/i;
|
|
332
|
+
for (const [filePath, content] of files) {
|
|
333
|
+
if (isTestFile(filePath)) continue;
|
|
334
|
+
if (!content) continue;
|
|
335
|
+
findings.push(
|
|
336
|
+
...scanLines(filePath, content, autoIndexPattern, 'Directory listing is enabled (autoIndex: true). Disable to prevent file enumeration.', 'definite')
|
|
337
|
+
);
|
|
338
|
+
if (filePath.includes('.htaccess') || filePath.includes('httpd.conf') || filePath.includes('apache')) {
|
|
339
|
+
findings.push(
|
|
340
|
+
...scanLines(filePath, content, apachePattern, 'Apache directory listing enabled via Options +Indexes. Use "Options -Indexes" instead.', 'definite')
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (filePath.includes('nginx') || filePath.endsWith('.conf')) {
|
|
344
|
+
findings.push(
|
|
345
|
+
...scanLines(filePath, content, nginxPattern, 'Nginx directory listing enabled. Set "autoindex off" to prevent file enumeration.', 'definite')
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return findings;
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// SEC-MISC-009: Default error pages (no custom error handler)
|
|
354
|
+
{
|
|
355
|
+
id: 'SEC-MISC-009',
|
|
356
|
+
category: 'security',
|
|
357
|
+
severity: 'low',
|
|
358
|
+
confidence: 'suggestion',
|
|
359
|
+
title: 'No custom error handler detected',
|
|
360
|
+
check({ files }) {
|
|
361
|
+
const findings = [];
|
|
362
|
+
// Look for Express apps without error-handling middleware
|
|
363
|
+
let isNextProject = false;
|
|
364
|
+
let hasErrorPage = false;
|
|
365
|
+
for (const [filePath, content] of files) {
|
|
366
|
+
if (isJS(filePath) && !isTestFile(filePath) && content && /(?:express\(\)|createServer)/.test(content)) {
|
|
367
|
+
const hasErrorHandler = /app\.use\s*\(\s*(?:function\s*\(\s*\w+\s*,\s*\w+\s*,\s*\w+\s*,\s*\w+|.*\(\s*\w+\s*,\s*\w+\s*,\s*\w+\s*,\s*\w+\s*\)\s*=>)/.test(content);
|
|
368
|
+
// Only flag files that define real application routes (not just
|
|
369
|
+
// health/status endpoints or pure app setup files)
|
|
370
|
+
const hasRoutes = /\.(?:get|post|put|patch|delete)\s*\(\s*['"]\//.test(content);
|
|
371
|
+
const onlyHealthRoutes = /\.(?:get|post)\s*\(\s*['"]\/(?:health|status|ping|ready|live)['"]/.test(content) &&
|
|
372
|
+
(content.match(/\.(?:get|post|put|patch|delete)\s*\(\s*['"]\/[^'"]+['"]/g) || []).length <= 1;
|
|
373
|
+
if (!hasErrorHandler && hasRoutes && !onlyHealthRoutes) {
|
|
374
|
+
findings.push({
|
|
375
|
+
file: filePath,
|
|
376
|
+
line: 1,
|
|
377
|
+
message: 'Express app without a custom error handler. Default error pages leak stack traces. Add a 4-argument error middleware.',
|
|
378
|
+
confidence: 'suggestion',
|
|
379
|
+
snippet: 'No (err, req, res, next) handler found',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (filePath.endsWith('next.config.js') || filePath.endsWith('next.config.mjs')) {
|
|
384
|
+
isNextProject = true;
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
filePath.includes('pages/_error') ||
|
|
388
|
+
filePath.includes('pages/500') ||
|
|
389
|
+
filePath.includes('app/error') ||
|
|
390
|
+
filePath.includes('app/global-error')
|
|
391
|
+
) {
|
|
392
|
+
hasErrorPage = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (isNextProject && !hasErrorPage) {
|
|
396
|
+
findings.push({
|
|
397
|
+
file: 'next.config.js',
|
|
398
|
+
line: 1,
|
|
399
|
+
message: 'Next.js project without custom error page. Add pages/_error.js or app/error.tsx to avoid leaking internal details.',
|
|
400
|
+
confidence: 'suggestion',
|
|
401
|
+
snippet: 'No custom error page found',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return findings;
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// SEC-MISC-010: Exposed env vars in Next.js via NEXT_PUBLIC_
|
|
409
|
+
{
|
|
410
|
+
id: 'SEC-MISC-010',
|
|
411
|
+
category: 'security',
|
|
412
|
+
severity: 'high',
|
|
413
|
+
confidence: 'likely',
|
|
414
|
+
title: 'Sensitive environment variable exposed via NEXT_PUBLIC_',
|
|
415
|
+
check({ files }) {
|
|
416
|
+
const findings = [];
|
|
417
|
+
const sensitiveNames = /NEXT_PUBLIC_(?:.*(?:SECRET|PASSWORD|PRIVATE|SERVICE_ROLE|ADMIN|DATABASE|DB_|SUPABASE_SERVICE|STRIPE_SECRET|API_SECRET))/i;
|
|
418
|
+
for (const [filePath, content] of files) {
|
|
419
|
+
if (isTestFile(filePath)) continue;
|
|
420
|
+
if (!content) continue;
|
|
421
|
+
if (!isJS(filePath) && !isConfig(filePath) && !filePath.endsWith('.env.example')) continue;
|
|
422
|
+
const lines = content.split('\n');
|
|
423
|
+
for (let i = 0; i < lines.length; i++) {
|
|
424
|
+
const line = lines[i];
|
|
425
|
+
if (shouldSkipLine(line)) continue;
|
|
426
|
+
if (sensitiveNames.test(line)) {
|
|
427
|
+
findings.push({
|
|
428
|
+
file: filePath,
|
|
429
|
+
line: i + 1,
|
|
430
|
+
message: 'Sensitive-looking variable prefixed with NEXT_PUBLIC_ will be exposed to the browser. Remove the prefix for server-only vars.',
|
|
431
|
+
confidence: 'likely',
|
|
432
|
+
snippet: line.trim().substring(0, 120),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return findings;
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// SEC-MISC-011: Public Docker registry push
|
|
442
|
+
{
|
|
443
|
+
id: 'SEC-MISC-011',
|
|
444
|
+
category: 'security',
|
|
445
|
+
severity: 'medium',
|
|
446
|
+
confidence: 'likely',
|
|
447
|
+
title: 'Docker image pushed to public registry',
|
|
448
|
+
check({ files }) {
|
|
449
|
+
const findings = [];
|
|
450
|
+
const pushPattern = /docker\s+push\s+(?!.*(?:\.azurecr\.io|\.ecr\.|gcr\.io|ghcr\.io|registry\.|localhost))/;
|
|
451
|
+
for (const [filePath, content] of files) {
|
|
452
|
+
if (isTestFile(filePath)) continue;
|
|
453
|
+
if (!content) continue;
|
|
454
|
+
if (
|
|
455
|
+
!filePath.endsWith('.yml') &&
|
|
456
|
+
!filePath.endsWith('.yaml') &&
|
|
457
|
+
!filePath.endsWith('.sh') &&
|
|
458
|
+
!filePath.includes('Makefile') &&
|
|
459
|
+
!isJS(filePath)
|
|
460
|
+
) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
findings.push(
|
|
464
|
+
...scanLines(
|
|
465
|
+
filePath, content,
|
|
466
|
+
pushPattern,
|
|
467
|
+
'Docker image appears to be pushed to a public registry (Docker Hub). Use a private registry for proprietary images.',
|
|
468
|
+
'likely'
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return findings;
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
// SEC-MISC-012: Open Redis/Memcached without auth
|
|
477
|
+
{
|
|
478
|
+
id: 'SEC-MISC-012',
|
|
479
|
+
category: 'security',
|
|
480
|
+
severity: 'high',
|
|
481
|
+
confidence: 'likely',
|
|
482
|
+
title: 'Redis/Memcached connection without authentication',
|
|
483
|
+
check({ files }) {
|
|
484
|
+
const findings = [];
|
|
485
|
+
for (const [filePath, content] of files) {
|
|
486
|
+
if (isTestFile(filePath)) continue;
|
|
487
|
+
if (!isJS(filePath)) continue;
|
|
488
|
+
if (!content) continue;
|
|
489
|
+
const lines = content.split('\n');
|
|
490
|
+
if (/(?:createClient|new Redis|ioredis)/i.test(content)) {
|
|
491
|
+
const hasAuth =
|
|
492
|
+
/(?:password|auth|requirepass|authPass)/i.test(content) ||
|
|
493
|
+
/rediss?:\/\/[^:]+:[^@]+@/.test(content);
|
|
494
|
+
if (!hasAuth) {
|
|
495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
496
|
+
if (/(?:createClient|new Redis)\s*\(/.test(lines[i])) {
|
|
497
|
+
findings.push({
|
|
498
|
+
file: filePath,
|
|
499
|
+
line: i + 1,
|
|
500
|
+
message: 'Redis client created without authentication. Set a password to prevent unauthorized access.',
|
|
501
|
+
confidence: 'suggestion',
|
|
502
|
+
snippet: lines[i].trim().substring(0, 120),
|
|
503
|
+
});
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return findings;
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
// SEC-MISC-013: Exposed Swagger/API docs without auth
|
|
515
|
+
{
|
|
516
|
+
id: 'SEC-MISC-013',
|
|
517
|
+
category: 'security',
|
|
518
|
+
severity: 'medium',
|
|
519
|
+
confidence: 'likely',
|
|
520
|
+
title: 'API documentation exposed without authentication',
|
|
521
|
+
check({ files }) {
|
|
522
|
+
const findings = [];
|
|
523
|
+
const swaggerPattern = /(?:swagger-ui|swaggerUi|openapi|\/api-docs|\/swagger|\/docs)/;
|
|
524
|
+
for (const [filePath, content] of files) {
|
|
525
|
+
if (isTestFile(filePath)) continue;
|
|
526
|
+
if (!isJS(filePath)) continue;
|
|
527
|
+
if (!content) continue;
|
|
528
|
+
const lines = content.split('\n');
|
|
529
|
+
for (let i = 0; i < lines.length; i++) {
|
|
530
|
+
const line = lines[i];
|
|
531
|
+
if (shouldSkipLine(line)) continue;
|
|
532
|
+
if (swaggerPattern.test(line) && /app\.use\s*\(/.test(line)) {
|
|
533
|
+
const authPattern = /(?:auth|authenticate|protect|guard|requireLogin|verifyToken)/i;
|
|
534
|
+
if (!authPattern.test(line)) {
|
|
535
|
+
const nearby = lines.slice(Math.max(0, i - 2), i + 1).join('\n');
|
|
536
|
+
if (!authPattern.test(nearby)) {
|
|
537
|
+
findings.push({
|
|
538
|
+
file: filePath,
|
|
539
|
+
line: i + 1,
|
|
540
|
+
message: 'Swagger/OpenAPI docs served without authentication. Protect API docs in production behind auth middleware.',
|
|
541
|
+
confidence: 'likely',
|
|
542
|
+
snippet: line.trim().substring(0, 120),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return findings;
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
// SEC-MISC-014: Open WebSocket without auth
|
|
554
|
+
{
|
|
555
|
+
id: 'SEC-MISC-014',
|
|
556
|
+
category: 'security',
|
|
557
|
+
severity: 'high',
|
|
558
|
+
confidence: 'likely',
|
|
559
|
+
title: 'WebSocket connection handler without authentication',
|
|
560
|
+
check({ files }) {
|
|
561
|
+
const findings = [];
|
|
562
|
+
for (const [filePath, content] of files) {
|
|
563
|
+
if (isTestFile(filePath)) continue;
|
|
564
|
+
if (!isJS(filePath)) continue;
|
|
565
|
+
if (!content) continue;
|
|
566
|
+
const wsServerPattern = /(?:new WebSocket\.Server|new WebSocketServer|wss\.on\s*\(\s*['"]connection['"]|io\.on\s*\(\s*['"]connection['"])/;
|
|
567
|
+
if (!wsServerPattern.test(content)) continue;
|
|
568
|
+
const authPattern = /(?:auth|authenticate|token|verify|jwt|session|cookie|handshake\.(?:auth|query))/i;
|
|
569
|
+
const lines = content.split('\n');
|
|
570
|
+
for (let i = 0; i < lines.length; i++) {
|
|
571
|
+
const line = lines[i];
|
|
572
|
+
if (shouldSkipLine(line)) continue;
|
|
573
|
+
if (/(?:\.on\s*\(\s*['"]connection['"]|\.on\s*\(\s*['"]connect['"])/.test(line)) {
|
|
574
|
+
const handlerBody = lines.slice(i, Math.min(lines.length, i + 15)).join('\n');
|
|
575
|
+
if (!authPattern.test(handlerBody)) {
|
|
576
|
+
findings.push({
|
|
577
|
+
file: filePath,
|
|
578
|
+
line: i + 1,
|
|
579
|
+
message: 'WebSocket connection handler lacks authentication. Verify tokens/sessions in the connection or upgrade handler.',
|
|
580
|
+
confidence: 'suggestion',
|
|
581
|
+
snippet: line.trim().substring(0, 120),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return findings;
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// SEC-MISC-015: Overly permissive IAM policy
|
|
592
|
+
{
|
|
593
|
+
id: 'SEC-MISC-015',
|
|
594
|
+
category: 'security',
|
|
595
|
+
severity: 'critical',
|
|
596
|
+
confidence: 'definite',
|
|
597
|
+
title: 'Overly permissive IAM policy detected',
|
|
598
|
+
check({ files }) {
|
|
599
|
+
const findings = [];
|
|
600
|
+
const wildActionPattern = /Action\s*[:=]\s*['"]?\*['"]?/;
|
|
601
|
+
const wildResourcePattern = /Resource\s*[:=]\s*['"]?\*['"]?/;
|
|
602
|
+
const allowEffect = /Effect\s*[:=]\s*['"]Allow['"]/;
|
|
603
|
+
for (const [filePath, content] of files) {
|
|
604
|
+
if (isTestFile(filePath)) continue;
|
|
605
|
+
if (!content) continue;
|
|
606
|
+
if (
|
|
607
|
+
!isJS(filePath) &&
|
|
608
|
+
!isConfig(filePath) &&
|
|
609
|
+
!filePath.endsWith('.tf') &&
|
|
610
|
+
!filePath.endsWith('.hcl')
|
|
611
|
+
) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const lines = content.split('\n');
|
|
615
|
+
// Check for Allow + Action: * combination
|
|
616
|
+
if (allowEffect.test(content) && wildActionPattern.test(content)) {
|
|
617
|
+
for (let i = 0; i < lines.length; i++) {
|
|
618
|
+
if (wildActionPattern.test(lines[i])) {
|
|
619
|
+
// Verify this is in the same policy block as Allow
|
|
620
|
+
const blockStart = Math.max(0, i - 10);
|
|
621
|
+
const blockEnd = Math.min(lines.length, i + 10);
|
|
622
|
+
const block = lines.slice(blockStart, blockEnd).join('\n');
|
|
623
|
+
if (allowEffect.test(block)) {
|
|
624
|
+
findings.push({
|
|
625
|
+
file: filePath,
|
|
626
|
+
line: i + 1,
|
|
627
|
+
message: 'IAM policy with Action: * and Effect: Allow grants full access. Follow least-privilege principle.',
|
|
628
|
+
confidence: 'definite',
|
|
629
|
+
snippet: lines[i].trim().substring(0, 120),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Check for Allow + Resource: * combination
|
|
636
|
+
if (allowEffect.test(content) && wildResourcePattern.test(content)) {
|
|
637
|
+
for (let i = 0; i < lines.length; i++) {
|
|
638
|
+
if (wildResourcePattern.test(lines[i])) {
|
|
639
|
+
const blockStart = Math.max(0, i - 10);
|
|
640
|
+
const blockEnd = Math.min(lines.length, i + 10);
|
|
641
|
+
const block = lines.slice(blockStart, blockEnd).join('\n');
|
|
642
|
+
if (allowEffect.test(block)) {
|
|
643
|
+
findings.push({
|
|
644
|
+
file: filePath,
|
|
645
|
+
line: i + 1,
|
|
646
|
+
message: 'IAM policy with Resource: * and Effect: Allow is overly permissive. Scope to specific resource ARNs.',
|
|
647
|
+
confidence: 'definite',
|
|
648
|
+
snippet: lines[i].trim().substring(0, 120),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return findings;
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
export default rules;
|