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,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope-Aware Rules (SCOPE-*)
|
|
3
|
+
*
|
|
4
|
+
* These rules leverage the lightweight scope analyzer to make context-sensitive
|
|
5
|
+
* decisions: detecting patterns that only matter inside certain blocks,
|
|
6
|
+
* suppressing false positives in test files / comments / strings, and
|
|
7
|
+
* evaluating structural quality metrics (function length, nesting depth, etc.).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { analyzeScope, languageFromPath } from '../scope-analyzer.js';
|
|
11
|
+
|
|
12
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
13
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
14
|
+
|
|
15
|
+
const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
|
|
16
|
+
|
|
17
|
+
// Cache scope analysis per file to avoid re-parsing
|
|
18
|
+
function getScope(cache, filePath, content) {
|
|
19
|
+
if (!cache.has(filePath)) {
|
|
20
|
+
cache.set(filePath, analyzeScope(content, languageFromPath(filePath)));
|
|
21
|
+
}
|
|
22
|
+
return cache.get(filePath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Security middleware names
|
|
26
|
+
const SECURITY_MIDDLEWARE = ['helmet', 'cors', 'csurf', 'express-rate-limit', 'rate-limiter-flexible'];
|
|
27
|
+
const INPUT_VALIDATION_LIBS = ['joi', 'zod', 'yup', 'ajv', 'express-validator', 'class-validator'];
|
|
28
|
+
const SECURITY_LIBS = [...SECURITY_MIDDLEWARE, ...INPUT_VALIDATION_LIBS, 'bcrypt', 'bcryptjs', 'argon2'];
|
|
29
|
+
|
|
30
|
+
// Database call patterns
|
|
31
|
+
const DB_CALL_PATTERN = /\b(?:\.query|\.execute|\.find|\.findOne|\.findMany|\.findAll|\.select|\.insert|\.update|\.delete|\.aggregate|\.raw|\.fetch|Model\.\w+|db\.\w+|collection\.\w+|cursor\.\w+)\s*\(/;
|
|
32
|
+
|
|
33
|
+
// Sync fs operations
|
|
34
|
+
const SYNC_FS_PATTERN = /\b(?:readFileSync|writeFileSync|appendFileSync|readdirSync|statSync|mkdirSync|existsSync|unlinkSync|renameSync|copyFileSync)\s*\(/;
|
|
35
|
+
|
|
36
|
+
const rules = [
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------
|
|
39
|
+
// SCOPE-AUTH-001: Route handler without auth middleware
|
|
40
|
+
// ---------------------------------------------------------------
|
|
41
|
+
{
|
|
42
|
+
id: 'SCOPE-AUTH-001',
|
|
43
|
+
category: 'security',
|
|
44
|
+
severity: 'high',
|
|
45
|
+
confidence: 'suggestion',
|
|
46
|
+
title: 'Route Handler Without Auth Middleware',
|
|
47
|
+
description: 'Express route handler detected with (req, res) parameters but no authentication middleware appears in the same router file.',
|
|
48
|
+
fix: { suggestion: 'Add authentication middleware (e.g., passport, express-jwt) to protect this route.' },
|
|
49
|
+
check({ files }) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
const scopeCache = new Map();
|
|
52
|
+
for (const [path, content] of files) {
|
|
53
|
+
if (!isJS(path)) continue;
|
|
54
|
+
const scope = getScope(scopeCache, path, content);
|
|
55
|
+
if (scope.isTestFile) continue;
|
|
56
|
+
|
|
57
|
+
// Check if file has any auth-related imports or middleware references
|
|
58
|
+
const hasAuth = /\b(auth|authenticate|passport|jwt|verify[Tt]oken|isAuthenticated|requireAuth|ensureAuth)\b/.test(content);
|
|
59
|
+
|
|
60
|
+
for (const handler of scope.routeHandlers) {
|
|
61
|
+
if (handler.method === 'use') continue; // middleware registration
|
|
62
|
+
if (!hasAuth) {
|
|
63
|
+
findings.push({
|
|
64
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
65
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
66
|
+
file: path, line: handler.startLine, fix: this.fix,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------
|
|
76
|
+
// SCOPE-AUTH-002: Route handler without rate limiting
|
|
77
|
+
// ---------------------------------------------------------------
|
|
78
|
+
{
|
|
79
|
+
id: 'SCOPE-AUTH-002',
|
|
80
|
+
category: 'security',
|
|
81
|
+
severity: 'medium',
|
|
82
|
+
confidence: 'suggestion',
|
|
83
|
+
title: 'Route Handler Without Rate Limiting',
|
|
84
|
+
description: 'Express route handler without rate limiting middleware imported in the same file.',
|
|
85
|
+
fix: { suggestion: 'Add rate limiting middleware (e.g., express-rate-limit) to prevent abuse.' },
|
|
86
|
+
check({ files }) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
const scopeCache = new Map();
|
|
89
|
+
for (const [path, content] of files) {
|
|
90
|
+
if (!isJS(path)) continue;
|
|
91
|
+
const scope = getScope(scopeCache, path, content);
|
|
92
|
+
if (scope.isTestFile) continue;
|
|
93
|
+
|
|
94
|
+
const hasRateLimit = scope.hasAnyImport(['express-rate-limit', 'rate-limiter-flexible']) ||
|
|
95
|
+
/\brateLimit|rateLimiter|limiter\b/.test(content);
|
|
96
|
+
|
|
97
|
+
for (const handler of scope.routeHandlers) {
|
|
98
|
+
if (handler.method === 'use') continue;
|
|
99
|
+
if (!hasRateLimit) {
|
|
100
|
+
findings.push({
|
|
101
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
102
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
103
|
+
file: path, line: handler.startLine, fix: this.fix,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------
|
|
113
|
+
// SCOPE-ERR-001: Async function without try/catch
|
|
114
|
+
// ---------------------------------------------------------------
|
|
115
|
+
{
|
|
116
|
+
id: 'SCOPE-ERR-001',
|
|
117
|
+
category: 'reliability',
|
|
118
|
+
severity: 'medium',
|
|
119
|
+
confidence: 'suggestion',
|
|
120
|
+
title: 'Async Function Without try/catch',
|
|
121
|
+
description: 'Async function does not contain a try/catch block. Unhandled rejections may crash the process.',
|
|
122
|
+
fix: { suggestion: 'Wrap the function body in a try/catch block or use an error-handling middleware.' },
|
|
123
|
+
check({ files }) {
|
|
124
|
+
const findings = [];
|
|
125
|
+
const scopeCache = new Map();
|
|
126
|
+
for (const [path, content] of files) {
|
|
127
|
+
if (!isJS(path)) continue;
|
|
128
|
+
const scope = getScope(scopeCache, path, content);
|
|
129
|
+
if (scope.isTestFile) continue;
|
|
130
|
+
|
|
131
|
+
for (const fn of scope.functions) {
|
|
132
|
+
if (!fn.isAsync) continue;
|
|
133
|
+
if (fn.endLine - fn.startLine < 3) continue; // trivial one-liners
|
|
134
|
+
|
|
135
|
+
// Check if any try/catch exists inside the function
|
|
136
|
+
const hasTryCatch = scope.tryCatches.some(
|
|
137
|
+
tc => tc.tryStart >= fn.startLine && tc.tryStart <= fn.endLine
|
|
138
|
+
);
|
|
139
|
+
if (!hasTryCatch) {
|
|
140
|
+
findings.push({
|
|
141
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
142
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
143
|
+
file: path, line: fn.startLine, fix: this.fix,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return findings;
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------
|
|
153
|
+
// SCOPE-ERR-002: Promise chain without .catch()
|
|
154
|
+
// ---------------------------------------------------------------
|
|
155
|
+
{
|
|
156
|
+
id: 'SCOPE-ERR-002',
|
|
157
|
+
category: 'reliability',
|
|
158
|
+
severity: 'medium',
|
|
159
|
+
confidence: 'suggestion',
|
|
160
|
+
title: 'Promise Chain Without .catch()',
|
|
161
|
+
description: 'Promise chain using .then() without a corresponding .catch() handler.',
|
|
162
|
+
fix: { suggestion: 'Add a .catch() handler or use async/await with try/catch.' },
|
|
163
|
+
check({ files }) {
|
|
164
|
+
const findings = [];
|
|
165
|
+
const scopeCache = new Map();
|
|
166
|
+
for (const [path, content] of files) {
|
|
167
|
+
if (!isJS(path)) continue;
|
|
168
|
+
const scope = getScope(scopeCache, path, content);
|
|
169
|
+
if (scope.isTestFile) continue;
|
|
170
|
+
|
|
171
|
+
const lines = content.split('\n');
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
const lineNum = i + 1;
|
|
174
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
175
|
+
const line = lines[i];
|
|
176
|
+
if (/\.then\s*\(/.test(line)) {
|
|
177
|
+
// Look ahead up to 5 lines for .catch
|
|
178
|
+
let hasCatch = false;
|
|
179
|
+
for (let j = i; j < Math.min(i + 6, lines.length); j++) {
|
|
180
|
+
if (/\.catch\s*\(/.test(lines[j])) {
|
|
181
|
+
hasCatch = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!hasCatch) {
|
|
186
|
+
findings.push({
|
|
187
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
188
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
189
|
+
file: path, line: lineNum, fix: this.fix,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return findings;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------
|
|
200
|
+
// SCOPE-ERR-003: Empty catch block
|
|
201
|
+
// ---------------------------------------------------------------
|
|
202
|
+
{
|
|
203
|
+
id: 'SCOPE-ERR-003',
|
|
204
|
+
category: 'reliability',
|
|
205
|
+
severity: 'medium',
|
|
206
|
+
confidence: 'likely',
|
|
207
|
+
title: 'Empty catch Block',
|
|
208
|
+
description: 'Catch block is empty or contains only a comment. Errors are silently swallowed.',
|
|
209
|
+
fix: { suggestion: 'Log the error or re-throw it. Silent swallowing hides bugs.' },
|
|
210
|
+
check({ files }) {
|
|
211
|
+
const findings = [];
|
|
212
|
+
const scopeCache = new Map();
|
|
213
|
+
for (const [path, content] of files) {
|
|
214
|
+
if (!isJS(path)) continue;
|
|
215
|
+
const scope = getScope(scopeCache, path, content);
|
|
216
|
+
if (scope.isTestFile) continue;
|
|
217
|
+
|
|
218
|
+
const lines = content.split('\n');
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
const lineNum = i + 1;
|
|
221
|
+
const trimmed = lines[i].trim();
|
|
222
|
+
// Match } catch (e) { } or catch(e) { }
|
|
223
|
+
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(trimmed)) {
|
|
224
|
+
findings.push({
|
|
225
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
226
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
227
|
+
file: path, line: lineNum, fix: this.fix,
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Multi-line empty catch: catch(...) { \n }
|
|
232
|
+
if (/catch\s*\([^)]*\)\s*\{\s*$/.test(trimmed)) {
|
|
233
|
+
// Look at the next non-blank line
|
|
234
|
+
let nextContentLine = '';
|
|
235
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
236
|
+
const t = lines[j].trim();
|
|
237
|
+
if (t === '') continue;
|
|
238
|
+
nextContentLine = t;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (nextContentLine === '}' || nextContentLine.startsWith('//') && i + 2 < lines.length) {
|
|
242
|
+
// Check if the line after comment is }
|
|
243
|
+
if (nextContentLine === '}') {
|
|
244
|
+
findings.push({
|
|
245
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
246
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
247
|
+
file: path, line: lineNum, fix: this.fix,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return findings;
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------
|
|
259
|
+
// SCOPE-TEST-001: Security-sensitive code in test file (suppress)
|
|
260
|
+
// ---------------------------------------------------------------
|
|
261
|
+
{
|
|
262
|
+
id: 'SCOPE-TEST-001',
|
|
263
|
+
category: 'security',
|
|
264
|
+
severity: 'info',
|
|
265
|
+
confidence: 'likely',
|
|
266
|
+
title: 'Security-Sensitive Pattern in Test File',
|
|
267
|
+
description: 'eval, exec, or raw SQL usage detected in a test file. This is expected for testing and should not be flagged as a vulnerability.',
|
|
268
|
+
check({ files }) {
|
|
269
|
+
// This rule returns nothing — it exists to document the suppression.
|
|
270
|
+
// Other scope rules already skip test files.
|
|
271
|
+
return [];
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------
|
|
276
|
+
// SCOPE-TEST-002: Hardcoded credentials NOT in test file
|
|
277
|
+
// ---------------------------------------------------------------
|
|
278
|
+
{
|
|
279
|
+
id: 'SCOPE-TEST-002',
|
|
280
|
+
category: 'security',
|
|
281
|
+
severity: 'high',
|
|
282
|
+
confidence: 'likely',
|
|
283
|
+
title: 'Hardcoded Credentials Outside Test Code',
|
|
284
|
+
description: 'Hardcoded password, secret, or API key found in production code (not in a test file).',
|
|
285
|
+
fix: { suggestion: 'Use environment variables or a secrets manager instead of hardcoded credentials.' },
|
|
286
|
+
check({ files }) {
|
|
287
|
+
const findings = [];
|
|
288
|
+
const scopeCache = new Map();
|
|
289
|
+
const credPattern = /(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\s*[:=]\s*['"][^'"]{4,}['"]/i;
|
|
290
|
+
|
|
291
|
+
for (const [path, content] of files) {
|
|
292
|
+
if (!isJS(path)) continue;
|
|
293
|
+
const scope = getScope(scopeCache, path, content);
|
|
294
|
+
if (scope.isTestFile) continue;
|
|
295
|
+
|
|
296
|
+
const lines = content.split('\n');
|
|
297
|
+
for (let i = 0; i < lines.length; i++) {
|
|
298
|
+
const lineNum = i + 1;
|
|
299
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
300
|
+
if (credPattern.test(lines[i])) {
|
|
301
|
+
findings.push({
|
|
302
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
303
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
304
|
+
file: path, line: lineNum, fix: this.fix,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return findings;
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------
|
|
314
|
+
// SCOPE-QUAL-001: Function exceeds 50 lines
|
|
315
|
+
// ---------------------------------------------------------------
|
|
316
|
+
{
|
|
317
|
+
id: 'SCOPE-QUAL-001',
|
|
318
|
+
category: 'quality',
|
|
319
|
+
severity: 'low',
|
|
320
|
+
confidence: 'definite',
|
|
321
|
+
title: 'Function Exceeds 50 Lines',
|
|
322
|
+
description: 'Long functions are harder to understand, test, and maintain.',
|
|
323
|
+
fix: { suggestion: 'Extract sub-operations into smaller helper functions.' },
|
|
324
|
+
check({ files }) {
|
|
325
|
+
const findings = [];
|
|
326
|
+
const scopeCache = new Map();
|
|
327
|
+
for (const [path, content] of files) {
|
|
328
|
+
if (!isJS(path)) continue;
|
|
329
|
+
const scope = getScope(scopeCache, path, content);
|
|
330
|
+
if (scope.isTestFile) continue;
|
|
331
|
+
|
|
332
|
+
for (const fn of scope.functions) {
|
|
333
|
+
const lineCount = fn.endLine - fn.startLine + 1;
|
|
334
|
+
if (lineCount > 50) {
|
|
335
|
+
findings.push({
|
|
336
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
337
|
+
title: this.title,
|
|
338
|
+
description: `Function "${fn.name}" is ${lineCount} lines long (limit: 50).`,
|
|
339
|
+
confidence: this.confidence,
|
|
340
|
+
file: path, line: fn.startLine, fix: this.fix,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return findings;
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------
|
|
350
|
+
// SCOPE-QUAL-002: Function has more than 5 parameters
|
|
351
|
+
// ---------------------------------------------------------------
|
|
352
|
+
{
|
|
353
|
+
id: 'SCOPE-QUAL-002',
|
|
354
|
+
category: 'quality',
|
|
355
|
+
severity: 'low',
|
|
356
|
+
confidence: 'definite',
|
|
357
|
+
title: 'Function Has More Than 5 Parameters',
|
|
358
|
+
description: 'Functions with many parameters are hard to call correctly and suggest the need for an options object.',
|
|
359
|
+
fix: { suggestion: 'Use an options/config object parameter instead of many positional parameters.' },
|
|
360
|
+
check({ files }) {
|
|
361
|
+
const findings = [];
|
|
362
|
+
const scopeCache = new Map();
|
|
363
|
+
for (const [path, content] of files) {
|
|
364
|
+
if (!isJS(path)) continue;
|
|
365
|
+
const scope = getScope(scopeCache, path, content);
|
|
366
|
+
if (scope.isTestFile) continue;
|
|
367
|
+
|
|
368
|
+
for (const fn of scope.functions) {
|
|
369
|
+
if (fn.paramCount > 5) {
|
|
370
|
+
findings.push({
|
|
371
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
372
|
+
title: this.title,
|
|
373
|
+
description: `Function "${fn.name}" has ${fn.paramCount} parameters (limit: 5).`,
|
|
374
|
+
confidence: this.confidence,
|
|
375
|
+
file: path, line: fn.startLine, fix: this.fix,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return findings;
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------
|
|
385
|
+
// SCOPE-QUAL-003: Nesting depth exceeds 4 levels
|
|
386
|
+
// ---------------------------------------------------------------
|
|
387
|
+
{
|
|
388
|
+
id: 'SCOPE-QUAL-003',
|
|
389
|
+
category: 'quality',
|
|
390
|
+
severity: 'low',
|
|
391
|
+
confidence: 'likely',
|
|
392
|
+
title: 'Excessive Nesting Depth',
|
|
393
|
+
description: 'Code nesting exceeds 4 levels, making it hard to follow.',
|
|
394
|
+
fix: { suggestion: 'Use early returns, extract helper functions, or use guard clauses to reduce nesting.' },
|
|
395
|
+
check({ files }) {
|
|
396
|
+
const findings = [];
|
|
397
|
+
const scopeCache = new Map();
|
|
398
|
+
const reported = new Set();
|
|
399
|
+
|
|
400
|
+
for (const [path, content] of files) {
|
|
401
|
+
if (!isJS(path)) continue;
|
|
402
|
+
const scope = getScope(scopeCache, path, content);
|
|
403
|
+
if (scope.isTestFile) continue;
|
|
404
|
+
|
|
405
|
+
for (let i = 0; i < scope.lineCount; i++) {
|
|
406
|
+
const lineNum = i + 1;
|
|
407
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
408
|
+
if (scope.lineDepth[i] > 4) {
|
|
409
|
+
const fn = scope.getFunctionAt(lineNum);
|
|
410
|
+
const key = `${path}:${fn ? fn.name : 'global'}`;
|
|
411
|
+
if (!reported.has(key)) {
|
|
412
|
+
reported.add(key);
|
|
413
|
+
findings.push({
|
|
414
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
415
|
+
title: this.title,
|
|
416
|
+
description: `Nesting depth of ${scope.lineDepth[i]} at line ${lineNum}${fn ? ` in function "${fn.name}"` : ''}.`,
|
|
417
|
+
confidence: this.confidence,
|
|
418
|
+
file: path, line: lineNum, fix: this.fix,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return findings;
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------
|
|
429
|
+
// SCOPE-QUAL-004: Class exceeds 300 lines
|
|
430
|
+
// ---------------------------------------------------------------
|
|
431
|
+
{
|
|
432
|
+
id: 'SCOPE-QUAL-004',
|
|
433
|
+
category: 'quality',
|
|
434
|
+
severity: 'low',
|
|
435
|
+
confidence: 'definite',
|
|
436
|
+
title: 'Class Exceeds 300 Lines',
|
|
437
|
+
description: 'Large classes often violate the Single Responsibility Principle.',
|
|
438
|
+
fix: { suggestion: 'Split the class into smaller, focused classes or use composition.' },
|
|
439
|
+
check({ files }) {
|
|
440
|
+
const findings = [];
|
|
441
|
+
const scopeCache = new Map();
|
|
442
|
+
for (const [path, content] of files) {
|
|
443
|
+
if (!isJS(path)) continue;
|
|
444
|
+
const scope = getScope(scopeCache, path, content);
|
|
445
|
+
if (scope.isTestFile) continue;
|
|
446
|
+
|
|
447
|
+
for (const cls of scope.classes) {
|
|
448
|
+
const lineCount = cls.endLine - cls.startLine + 1;
|
|
449
|
+
if (lineCount > 300) {
|
|
450
|
+
findings.push({
|
|
451
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
452
|
+
title: this.title,
|
|
453
|
+
description: `Class "${cls.name}" is ${lineCount} lines long (limit: 300).`,
|
|
454
|
+
confidence: this.confidence,
|
|
455
|
+
file: path, line: cls.startLine, fix: this.fix,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return findings;
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------
|
|
465
|
+
// SCOPE-PERF-001: Database call inside a loop (N+1 query)
|
|
466
|
+
// ---------------------------------------------------------------
|
|
467
|
+
{
|
|
468
|
+
id: 'SCOPE-PERF-001',
|
|
469
|
+
category: 'performance',
|
|
470
|
+
severity: 'high',
|
|
471
|
+
confidence: 'likely',
|
|
472
|
+
title: 'Database Call Inside Loop (N+1 Query)',
|
|
473
|
+
description: 'A database query is executed inside a loop, causing N+1 query performance problems.',
|
|
474
|
+
fix: { suggestion: 'Batch the queries using WHERE IN, Promise.all, or eager loading.' },
|
|
475
|
+
check({ files }) {
|
|
476
|
+
const findings = [];
|
|
477
|
+
const scopeCache = new Map();
|
|
478
|
+
for (const [path, content] of files) {
|
|
479
|
+
if (!isJS(path)) continue;
|
|
480
|
+
const scope = getScope(scopeCache, path, content);
|
|
481
|
+
if (scope.isTestFile) continue;
|
|
482
|
+
|
|
483
|
+
const lines = content.split('\n');
|
|
484
|
+
for (let i = 0; i < lines.length; i++) {
|
|
485
|
+
const lineNum = i + 1;
|
|
486
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
487
|
+
if (scope.isInsideLoop(lineNum) && DB_CALL_PATTERN.test(lines[i])) {
|
|
488
|
+
findings.push({
|
|
489
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
490
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
491
|
+
file: path, line: lineNum, fix: this.fix,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return findings;
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------
|
|
501
|
+
// SCOPE-PERF-002: await inside a loop
|
|
502
|
+
// ---------------------------------------------------------------
|
|
503
|
+
{
|
|
504
|
+
id: 'SCOPE-PERF-002',
|
|
505
|
+
category: 'performance',
|
|
506
|
+
severity: 'medium',
|
|
507
|
+
confidence: 'likely',
|
|
508
|
+
title: 'await Inside Loop',
|
|
509
|
+
description: 'Using await inside a loop serializes async operations. Consider using Promise.all for parallel execution.',
|
|
510
|
+
fix: { suggestion: 'Collect promises and use Promise.all() instead of awaiting each one sequentially.' },
|
|
511
|
+
check({ files }) {
|
|
512
|
+
const findings = [];
|
|
513
|
+
const scopeCache = new Map();
|
|
514
|
+
for (const [path, content] of files) {
|
|
515
|
+
if (!isJS(path)) continue;
|
|
516
|
+
const scope = getScope(scopeCache, path, content);
|
|
517
|
+
if (scope.isTestFile) continue;
|
|
518
|
+
|
|
519
|
+
const lines = content.split('\n');
|
|
520
|
+
for (let i = 0; i < lines.length; i++) {
|
|
521
|
+
const lineNum = i + 1;
|
|
522
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
523
|
+
if (scope.isInsideLoop(lineNum) && /\bawait\s/.test(lines[i])) {
|
|
524
|
+
findings.push({
|
|
525
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
526
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
527
|
+
file: path, line: lineNum, fix: this.fix,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return findings;
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------
|
|
537
|
+
// SCOPE-PERF-003: Synchronous fs operation inside async function
|
|
538
|
+
// ---------------------------------------------------------------
|
|
539
|
+
{
|
|
540
|
+
id: 'SCOPE-PERF-003',
|
|
541
|
+
category: 'performance',
|
|
542
|
+
severity: 'medium',
|
|
543
|
+
confidence: 'likely',
|
|
544
|
+
title: 'Synchronous fs Operation in Async Function',
|
|
545
|
+
description: 'Synchronous file system calls block the event loop. Use async alternatives inside async functions.',
|
|
546
|
+
fix: { suggestion: 'Use fs.promises (readFile, writeFile, etc.) instead of synchronous variants.' },
|
|
547
|
+
check({ files }) {
|
|
548
|
+
const findings = [];
|
|
549
|
+
const scopeCache = new Map();
|
|
550
|
+
for (const [path, content] of files) {
|
|
551
|
+
if (!isJS(path)) continue;
|
|
552
|
+
const scope = getScope(scopeCache, path, content);
|
|
553
|
+
if (scope.isTestFile) continue;
|
|
554
|
+
|
|
555
|
+
const lines = content.split('\n');
|
|
556
|
+
for (let i = 0; i < lines.length; i++) {
|
|
557
|
+
const lineNum = i + 1;
|
|
558
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
559
|
+
|
|
560
|
+
const fn = scope.getFunctionAt(lineNum);
|
|
561
|
+
if (fn && fn.isAsync && SYNC_FS_PATTERN.test(lines[i])) {
|
|
562
|
+
findings.push({
|
|
563
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
564
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
565
|
+
file: path, line: lineNum, fix: this.fix,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return findings;
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
// ---------------------------------------------------------------
|
|
575
|
+
// SCOPE-SEC-001: eval/exec not inside sandboxed context
|
|
576
|
+
// ---------------------------------------------------------------
|
|
577
|
+
{
|
|
578
|
+
id: 'SCOPE-SEC-001',
|
|
579
|
+
category: 'security',
|
|
580
|
+
severity: 'critical',
|
|
581
|
+
confidence: 'likely',
|
|
582
|
+
title: 'eval/exec Outside Sandboxed Context',
|
|
583
|
+
description: 'eval() or exec() used outside of a sandboxing function (vm.runInNewContext, sandbox, etc.).',
|
|
584
|
+
fix: { suggestion: 'Use a sandboxed execution environment (vm2, isolated-vm) instead of eval/exec.' },
|
|
585
|
+
check({ files }) {
|
|
586
|
+
const findings = [];
|
|
587
|
+
const scopeCache = new Map();
|
|
588
|
+
for (const [path, content] of files) {
|
|
589
|
+
if (!isJS(path)) continue;
|
|
590
|
+
const scope = getScope(scopeCache, path, content);
|
|
591
|
+
if (scope.isTestFile) continue;
|
|
592
|
+
|
|
593
|
+
const hasSandbox = /\b(vm\.runInNewContext|vm\.createContext|isolated-vm|vm2|sandbox)\b/.test(content);
|
|
594
|
+
|
|
595
|
+
const lines = content.split('\n');
|
|
596
|
+
for (let i = 0; i < lines.length; i++) {
|
|
597
|
+
const lineNum = i + 1;
|
|
598
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
599
|
+
if (/\b(eval|Function)\s*\(/.test(lines[i]) || /\bexec\s*\(/.test(lines[i])) {
|
|
600
|
+
if (!hasSandbox) {
|
|
601
|
+
findings.push({
|
|
602
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
603
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
604
|
+
file: path, line: lineNum, fix: this.fix,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return findings;
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
// ---------------------------------------------------------------
|
|
615
|
+
// SCOPE-SEC-002: crypto.createHash outside hashing utility
|
|
616
|
+
// ---------------------------------------------------------------
|
|
617
|
+
{
|
|
618
|
+
id: 'SCOPE-SEC-002',
|
|
619
|
+
category: 'security',
|
|
620
|
+
severity: 'low',
|
|
621
|
+
confidence: 'suggestion',
|
|
622
|
+
title: 'crypto.createHash Outside Hashing Utility',
|
|
623
|
+
description: 'Direct use of crypto.createHash should be centralized in a utility function for consistency and to ease algorithm migration.',
|
|
624
|
+
fix: { suggestion: 'Wrap hashing in a dedicated utility function (e.g., hashPassword, computeHash).' },
|
|
625
|
+
check({ files }) {
|
|
626
|
+
const findings = [];
|
|
627
|
+
const scopeCache = new Map();
|
|
628
|
+
for (const [path, content] of files) {
|
|
629
|
+
if (!isJS(path)) continue;
|
|
630
|
+
const scope = getScope(scopeCache, path, content);
|
|
631
|
+
if (scope.isTestFile) continue;
|
|
632
|
+
|
|
633
|
+
const lines = content.split('\n');
|
|
634
|
+
for (let i = 0; i < lines.length; i++) {
|
|
635
|
+
const lineNum = i + 1;
|
|
636
|
+
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
637
|
+
if (/crypto\.createHash\s*\(/.test(lines[i])) {
|
|
638
|
+
const fn = scope.getFunctionAt(lineNum);
|
|
639
|
+
const isInHashUtil = fn && /hash/i.test(fn.name);
|
|
640
|
+
if (!isInHashUtil) {
|
|
641
|
+
findings.push({
|
|
642
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
643
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
644
|
+
file: path, line: lineNum, fix: this.fix,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return findings;
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
// ---------------------------------------------------------------
|
|
655
|
+
// SCOPE-IMPORT-001: Security middleware imported but not used
|
|
656
|
+
// ---------------------------------------------------------------
|
|
657
|
+
{
|
|
658
|
+
id: 'SCOPE-IMPORT-001',
|
|
659
|
+
category: 'security',
|
|
660
|
+
severity: 'medium',
|
|
661
|
+
confidence: 'likely',
|
|
662
|
+
title: 'Security Middleware Imported But Not Used',
|
|
663
|
+
description: 'A security middleware package (helmet, cors, csurf) is imported but never called in the file.',
|
|
664
|
+
fix: { suggestion: 'Call the imported middleware with app.use() to activate it.' },
|
|
665
|
+
check({ files }) {
|
|
666
|
+
const findings = [];
|
|
667
|
+
const scopeCache = new Map();
|
|
668
|
+
for (const [path, content] of files) {
|
|
669
|
+
if (!isJS(path)) continue;
|
|
670
|
+
const scope = getScope(scopeCache, path, content);
|
|
671
|
+
if (scope.isTestFile) continue;
|
|
672
|
+
|
|
673
|
+
for (const mw of SECURITY_MIDDLEWARE) {
|
|
674
|
+
if (scope.hasImport(mw)) {
|
|
675
|
+
// Check if actually used (called) beyond the import line
|
|
676
|
+
const importLine = scope.imports.find(imp => imp.module === mw)?.line || 0;
|
|
677
|
+
const name = mw.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); // camelCase
|
|
678
|
+
const usagePattern = new RegExp(`\\b(?:${name}|${mw.replace(/-/g, '')})\\s*\\(`);
|
|
679
|
+
let used = false;
|
|
680
|
+
const lines = content.split('\n');
|
|
681
|
+
for (let i = 0; i < lines.length; i++) {
|
|
682
|
+
if (i + 1 === importLine) continue; // skip the import line itself
|
|
683
|
+
if (usagePattern.test(lines[i])) { used = true; break; }
|
|
684
|
+
}
|
|
685
|
+
// Also check for app.use(name) pattern
|
|
686
|
+
if (!used) {
|
|
687
|
+
const usePattern = new RegExp(`app\\.use\\s*\\(.*${name}|app\\.use\\s*\\(.*${mw.replace(/-/g, '')}`);
|
|
688
|
+
used = usePattern.test(content);
|
|
689
|
+
}
|
|
690
|
+
if (!used) {
|
|
691
|
+
findings.push({
|
|
692
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
693
|
+
title: this.title,
|
|
694
|
+
description: `"${mw}" is imported but never activated. Call it with app.use().`,
|
|
695
|
+
confidence: this.confidence,
|
|
696
|
+
file: path, line: importLine, fix: this.fix,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return findings;
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
// ---------------------------------------------------------------
|
|
707
|
+
// SCOPE-IMPORT-002: No input validation library in Express app
|
|
708
|
+
// ---------------------------------------------------------------
|
|
709
|
+
{
|
|
710
|
+
id: 'SCOPE-IMPORT-002',
|
|
711
|
+
category: 'security',
|
|
712
|
+
severity: 'medium',
|
|
713
|
+
confidence: 'suggestion',
|
|
714
|
+
title: 'No Input Validation Library in Express App',
|
|
715
|
+
description: 'Express route file without any input validation library (joi, zod, yup, express-validator).',
|
|
716
|
+
fix: { suggestion: 'Add input validation using joi, zod, yup, or express-validator to validate request data.' },
|
|
717
|
+
check({ files }) {
|
|
718
|
+
const findings = [];
|
|
719
|
+
const scopeCache = new Map();
|
|
720
|
+
for (const [path, content] of files) {
|
|
721
|
+
if (!isJS(path)) continue;
|
|
722
|
+
const scope = getScope(scopeCache, path, content);
|
|
723
|
+
if (scope.isTestFile) continue;
|
|
724
|
+
if (scope.routeHandlers.length === 0) continue;
|
|
725
|
+
|
|
726
|
+
const hasValidation = scope.hasAnyImport(INPUT_VALIDATION_LIBS);
|
|
727
|
+
if (!hasValidation) {
|
|
728
|
+
findings.push({
|
|
729
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
730
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
731
|
+
file: path, line: 1, fix: this.fix,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return findings;
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------
|
|
740
|
+
// SCOPE-COMMENT-001: TODO/FIXME/HACK in exported function
|
|
741
|
+
// ---------------------------------------------------------------
|
|
742
|
+
{
|
|
743
|
+
id: 'SCOPE-COMMENT-001',
|
|
744
|
+
category: 'quality',
|
|
745
|
+
severity: 'low',
|
|
746
|
+
confidence: 'likely',
|
|
747
|
+
title: 'TODO/FIXME/HACK in Exported Function',
|
|
748
|
+
description: 'Production debt: TODO, FIXME, or HACK comment found inside an exported function.',
|
|
749
|
+
fix: { suggestion: 'Resolve the TODO/FIXME before shipping or create a tracked issue.' },
|
|
750
|
+
check({ files }) {
|
|
751
|
+
const findings = [];
|
|
752
|
+
const scopeCache = new Map();
|
|
753
|
+
const todoPattern = /\b(TODO|FIXME|HACK|XXX)\b/;
|
|
754
|
+
|
|
755
|
+
for (const [path, content] of files) {
|
|
756
|
+
if (!isJS(path)) continue;
|
|
757
|
+
const scope = getScope(scopeCache, path, content);
|
|
758
|
+
if (scope.isTestFile) continue;
|
|
759
|
+
|
|
760
|
+
const lines = content.split('\n');
|
|
761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
762
|
+
const lineNum = i + 1;
|
|
763
|
+
if (!todoPattern.test(lines[i])) continue;
|
|
764
|
+
if (scope.isExported(lineNum)) {
|
|
765
|
+
findings.push({
|
|
766
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
767
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
768
|
+
file: path, line: lineNum, fix: this.fix,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return findings;
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
// ---------------------------------------------------------------
|
|
778
|
+
// SCOPE-COMMENT-002: Security-related TODO comment
|
|
779
|
+
// ---------------------------------------------------------------
|
|
780
|
+
{
|
|
781
|
+
id: 'SCOPE-COMMENT-002',
|
|
782
|
+
category: 'security',
|
|
783
|
+
severity: 'high',
|
|
784
|
+
confidence: 'likely',
|
|
785
|
+
title: 'Security-Related TODO Comment',
|
|
786
|
+
description: 'A comment explicitly mentions missing security measures (auth, validation, encryption, sanitization).',
|
|
787
|
+
fix: { suggestion: 'Implement the security measure described in the TODO comment before deploying.' },
|
|
788
|
+
check({ files }) {
|
|
789
|
+
const findings = [];
|
|
790
|
+
const scopeCache = new Map();
|
|
791
|
+
const secTodoPattern = /(?:TODO|FIXME|HACK|XXX)\s*:?\s*.*\b(auth|authenticat|authoriz|validat|sanitiz|encrypt|secur|csrf|xss|inject|permission|access.control|rate.limit)/i;
|
|
792
|
+
|
|
793
|
+
for (const [path, content] of files) {
|
|
794
|
+
if (!isJS(path)) continue;
|
|
795
|
+
const scope = getScope(scopeCache, path, content);
|
|
796
|
+
if (scope.isTestFile) continue;
|
|
797
|
+
|
|
798
|
+
const lines = content.split('\n');
|
|
799
|
+
for (let i = 0; i < lines.length; i++) {
|
|
800
|
+
const lineNum = i + 1;
|
|
801
|
+
if (secTodoPattern.test(lines[i])) {
|
|
802
|
+
findings.push({
|
|
803
|
+
ruleId: this.id, category: this.category, severity: this.severity,
|
|
804
|
+
title: this.title, description: this.description, confidence: this.confidence,
|
|
805
|
+
file: path, line: lineNum, fix: this.fix,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return findings;
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
];
|
|
814
|
+
|
|
815
|
+
export default rules;
|