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,203 @@
|
|
|
1
|
+
// Bug detection: Database/ORM patterns
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
5
|
+
|
|
6
|
+
const rules = [
|
|
7
|
+
{
|
|
8
|
+
id: 'BUG-DB-001',
|
|
9
|
+
category: 'bugs',
|
|
10
|
+
severity: 'high',
|
|
11
|
+
confidence: 'likely',
|
|
12
|
+
title: 'N+1 query — database call inside loop',
|
|
13
|
+
check({ files }) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
for (const [fp, content] of files) {
|
|
16
|
+
if (!isJS(fp)) continue;
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
if (/\b(?:for|forEach|map)\b/.test(lines[i]) || /\.forEach\s*\(|\.map\s*\(/.test(lines[i])) {
|
|
20
|
+
let block = '';
|
|
21
|
+
let braceDepth = 0;
|
|
22
|
+
for (let j = i; j < Math.min(i + 15, lines.length); j++) {
|
|
23
|
+
block += lines[j] + '\n';
|
|
24
|
+
braceDepth += (lines[j].match(/\{/g) || []).length - (lines[j].match(/\}/g) || []).length;
|
|
25
|
+
if (braceDepth <= 0 && j > i) break;
|
|
26
|
+
}
|
|
27
|
+
// Database calls inside loop
|
|
28
|
+
if (/await\s+.*\.(findOne|findById|findUnique|findFirst|find\(|query\(|get\(|select\(|where\(|exec\()/.test(block)) {
|
|
29
|
+
findings.push({
|
|
30
|
+
ruleId: 'BUG-DB-001', category: 'bugs', severity: 'high',
|
|
31
|
+
title: 'N+1 query — database call inside loop',
|
|
32
|
+
description: 'Each loop iteration makes a separate DB query. Fetch all needed data in one query before the loop (e.g., findMany with where IN).',
|
|
33
|
+
file: fp, line: i + 1, fix: null,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return findings;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'BUG-DB-002',
|
|
44
|
+
category: 'bugs',
|
|
45
|
+
severity: 'high',
|
|
46
|
+
confidence: 'likely',
|
|
47
|
+
title: 'Multiple related DB operations without transaction',
|
|
48
|
+
check({ files }) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
for (const [fp, content] of files) {
|
|
51
|
+
if (!isJS(fp)) continue;
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
// Look for sequences of await db operations
|
|
55
|
+
if (/await\s+.*\.(create|update|delete|insert|remove|save|destroy)\s*\(/.test(lines[i])) {
|
|
56
|
+
let mutationCount = 1;
|
|
57
|
+
for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
|
|
58
|
+
if (/await\s+.*\.(create|update|delete|insert|remove|save|destroy)\s*\(/.test(lines[j])) {
|
|
59
|
+
mutationCount++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (mutationCount >= 2) {
|
|
63
|
+
// Check if wrapped in transaction
|
|
64
|
+
const context = lines.slice(Math.max(0, i - 5), Math.min(i + 15, lines.length)).join('\n');
|
|
65
|
+
if (!/transaction|prisma\.\$transaction|knex\.transaction|sequelize\.transaction|BEGIN|COMMIT|session\.startTransaction/.test(context)) {
|
|
66
|
+
findings.push({
|
|
67
|
+
ruleId: 'BUG-DB-002', category: 'bugs', severity: 'high',
|
|
68
|
+
title: `${mutationCount} mutations without transaction — partial failure leaves inconsistent data`,
|
|
69
|
+
description: 'Multiple write operations should be wrapped in a transaction so they either all succeed or all fail.',
|
|
70
|
+
file: fp, line: i + 1, fix: null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return findings;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'BUG-DB-003',
|
|
82
|
+
category: 'bugs',
|
|
83
|
+
severity: 'high',
|
|
84
|
+
confidence: 'likely',
|
|
85
|
+
title: 'findMany/find() without limit — returns entire table',
|
|
86
|
+
check({ files }) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
for (const [fp, content] of files) {
|
|
89
|
+
if (!isJS(fp)) continue;
|
|
90
|
+
if (!/prisma|mongoose|sequelize|knex|typeorm/i.test(content)) continue;
|
|
91
|
+
const lines = content.split('\n');
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
// findMany() or .find({}) without take/limit
|
|
94
|
+
if (/\.(findMany|find)\s*\(\s*(?:\{[^}]*\})?\s*\)/.test(lines[i])) {
|
|
95
|
+
const block = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');
|
|
96
|
+
if (!/take\s*:|limit\s*:|\.limit\s*\(|\$limit|skip|cursor|page|paginate/i.test(block)) {
|
|
97
|
+
findings.push({
|
|
98
|
+
ruleId: 'BUG-DB-003', category: 'bugs', severity: 'high',
|
|
99
|
+
title: 'Query returns all records without limit — crashes with large tables',
|
|
100
|
+
description: 'Unbounded queries return the entire table into memory. Add take/limit to prevent OOM errors on large datasets.',
|
|
101
|
+
file: fp, line: i + 1, fix: null,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'BUG-DB-004',
|
|
112
|
+
category: 'bugs',
|
|
113
|
+
severity: 'medium',
|
|
114
|
+
confidence: 'likely',
|
|
115
|
+
title: 'Sensitive data selected without field filtering',
|
|
116
|
+
check({ files }) {
|
|
117
|
+
const findings = [];
|
|
118
|
+
for (const [fp, content] of files) {
|
|
119
|
+
if (!isJS(fp)) continue;
|
|
120
|
+
const lines = content.split('\n');
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
// User.findOne() or prisma.user.findFirst() without select
|
|
123
|
+
if (/(?:User|user|Account|account|Customer|customer)\.(findOne|findFirst|findUnique|findMany)\s*\(\s*\{/.test(lines[i])) {
|
|
124
|
+
const block = lines.slice(i, Math.min(i + 8, lines.length)).join(' ');
|
|
125
|
+
if (!/select\s*:|\.select\s*\(|attributes\s*:/.test(block)) {
|
|
126
|
+
// Check if the result is sent to client
|
|
127
|
+
const afterBlock = lines.slice(i, Math.min(i + 12, lines.length)).join(' ');
|
|
128
|
+
if (/res\.(json|send)|return\s+.*user|response\.json/.test(afterBlock)) {
|
|
129
|
+
findings.push({
|
|
130
|
+
ruleId: 'BUG-DB-004', category: 'bugs', severity: 'medium',
|
|
131
|
+
title: 'User query without field selection — password/secrets may leak to client',
|
|
132
|
+
description: 'Querying user records without select/attributes returns all fields including password hashes and tokens. Use select to return only needed fields.',
|
|
133
|
+
file: fp, line: i + 1, fix: null,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return findings;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'BUG-DB-005',
|
|
145
|
+
category: 'bugs',
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
confidence: 'definite',
|
|
148
|
+
title: 'Raw SQL with string interpolation',
|
|
149
|
+
check({ files }) {
|
|
150
|
+
const findings = [];
|
|
151
|
+
for (const [fp, content] of files) {
|
|
152
|
+
if (!isJS(fp)) continue;
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
155
|
+
// prisma.$queryRaw`SELECT ... ${userInput}` or knex.raw(`...${var}`)
|
|
156
|
+
// db.query(`SELECT ... ${var}`)
|
|
157
|
+
if (/\.\$?(?:queryRaw|executeRaw|raw|query)\s*\(\s*`[^`]*\$\{/.test(lines[i])) {
|
|
158
|
+
// Allow tagged template literals (Prisma.sql, Prisma.$queryRaw)
|
|
159
|
+
if (!/Prisma\.sql|sql`/.test(lines[i]) && !/Prisma\.\$queryRaw`/.test(lines[i])) {
|
|
160
|
+
findings.push({
|
|
161
|
+
ruleId: 'BUG-DB-005', category: 'bugs', severity: 'high',
|
|
162
|
+
title: 'Raw SQL with template literal interpolation — SQL injection',
|
|
163
|
+
description: 'Template literals in raw queries are NOT parameterized. Use Prisma.sql`` tagged template or parameterized queries ($1, ?, :param).',
|
|
164
|
+
file: fp, line: i + 1, fix: null,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return findings;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'BUG-DB-006',
|
|
175
|
+
category: 'bugs',
|
|
176
|
+
severity: 'medium',
|
|
177
|
+
confidence: 'likely',
|
|
178
|
+
title: 'Database connection not closed on error',
|
|
179
|
+
check({ files }) {
|
|
180
|
+
const findings = [];
|
|
181
|
+
for (const [fp, content] of files) {
|
|
182
|
+
if (!isJS(fp)) continue;
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
// mongoose.connect or createConnection without disconnect/close on process exit
|
|
186
|
+
if (/(?:mongoose|pg|mysql|redis)\.(?:connect|createConnection|createClient|createPool)\s*\(/.test(lines[i])) {
|
|
187
|
+
if (!/process\.on\s*\(\s*['"](?:SIGINT|SIGTERM|exit)['"]/.test(content) && !/\.close\s*\(|\.disconnect\s*\(|\.end\s*\(|\.destroy\s*\(/.test(content)) {
|
|
188
|
+
findings.push({
|
|
189
|
+
ruleId: 'BUG-DB-006', category: 'bugs', severity: 'medium',
|
|
190
|
+
title: 'Database connection never closed — leaks connections on shutdown',
|
|
191
|
+
description: 'Add process.on("SIGTERM", () => connection.close()) to cleanly disconnect on shutdown. Otherwise connections pile up.',
|
|
192
|
+
file: fp, line: i + 1, fix: null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return findings;
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
export default rules;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Bug detection: JavaScript error handling bugs
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-ERR-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'throw string instead of Error — loses stack trace',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
if (/\bthrow\s+['"`]/.test(lines[i])) {
|
|
19
|
+
findings.push({
|
|
20
|
+
ruleId: 'BUG-ERR-001', category: 'bugs', severity: 'high',
|
|
21
|
+
title: 'throw "string" — no stack trace, instanceof Error fails',
|
|
22
|
+
description: 'Throwing a string instead of an Error object loses the stack trace and breaks catch(e) { if (e instanceof Error) } patterns. Use throw new Error("message").',
|
|
23
|
+
file: fp, line: i + 1, fix: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'BUG-ERR-002',
|
|
33
|
+
category: 'bugs',
|
|
34
|
+
severity: 'high',
|
|
35
|
+
confidence: 'likely',
|
|
36
|
+
title: 'catch rethrows without original error — loses context',
|
|
37
|
+
check({ files }) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const [fp, content] of files) {
|
|
40
|
+
if (!isJS(fp)) continue;
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
if (/\bcatch\s*\(\s*(\w+)\s*\)/.test(lines[i])) {
|
|
44
|
+
const errVar = lines[i].match(/catch\s*\(\s*(\w+)/)[1];
|
|
45
|
+
const body = lines.slice(i + 1, Math.min(lines.length, i + 10)).join('\n');
|
|
46
|
+
// Check for throw new Error("...") without { cause: err }
|
|
47
|
+
if (/throw\s+new\s+Error\s*\(/.test(body) && !body.includes(errVar) && !body.includes('cause')) {
|
|
48
|
+
findings.push({
|
|
49
|
+
ruleId: 'BUG-ERR-002', category: 'bugs', severity: 'high',
|
|
50
|
+
title: 'catch block throws new error without original cause',
|
|
51
|
+
description: `Caught error "${errVar}" is discarded. Pass the original: throw new Error("msg", { cause: ${errVar} })`,
|
|
52
|
+
file: fp, line: i + 1, fix: null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return findings;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'BUG-ERR-003',
|
|
63
|
+
category: 'bugs',
|
|
64
|
+
severity: 'high',
|
|
65
|
+
confidence: 'likely',
|
|
66
|
+
title: 'Error created but not thrown — new Error() without throw',
|
|
67
|
+
check({ files }) {
|
|
68
|
+
const findings = [];
|
|
69
|
+
for (const [fp, content] of files) {
|
|
70
|
+
if (!isJS(fp)) continue;
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
const line = lines[i];
|
|
74
|
+
if (/\bnew\s+(?:Error|TypeError|RangeError|SyntaxError)\s*\(/.test(line) && !line.includes('throw') && !line.includes('reject') && !line.includes('=')) {
|
|
75
|
+
findings.push({
|
|
76
|
+
ruleId: 'BUG-ERR-003', category: 'bugs', severity: 'high',
|
|
77
|
+
title: 'Error created but not thrown — new Error() has no effect',
|
|
78
|
+
description: 'Creating an Error object without throwing it does nothing. Add throw keyword: throw new Error("message").',
|
|
79
|
+
file: fp, line: i + 1, fix: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return findings;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'BUG-ERR-004',
|
|
89
|
+
category: 'bugs',
|
|
90
|
+
severity: 'high',
|
|
91
|
+
confidence: 'likely',
|
|
92
|
+
title: 'console.log(err) instead of console.error — loses error stream',
|
|
93
|
+
check({ files }) {
|
|
94
|
+
const findings = [];
|
|
95
|
+
for (const [fp, content] of files) {
|
|
96
|
+
if (!isJS(fp)) continue;
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
let inCatch = false;
|
|
99
|
+
let catchVar = null;
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)\s*\)/);
|
|
102
|
+
if (catchMatch) { inCatch = true; catchVar = catchMatch[1]; }
|
|
103
|
+
if (inCatch && catchVar && lines[i].includes('console.log') && lines[i].includes(catchVar)) {
|
|
104
|
+
findings.push({
|
|
105
|
+
ruleId: 'BUG-ERR-004', category: 'bugs', severity: 'high',
|
|
106
|
+
title: 'console.log() for error — use console.error() for stderr',
|
|
107
|
+
description: 'Errors logged with console.log go to stdout, not stderr. Use console.error() so errors are captured by log aggregators and CI.',
|
|
108
|
+
file: fp, line: i + 1, fix: null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (/^\s*\}/.test(lines[i]) && inCatch) inCatch = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return findings;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'BUG-ERR-005',
|
|
119
|
+
category: 'bugs',
|
|
120
|
+
severity: 'high',
|
|
121
|
+
confidence: 'likely',
|
|
122
|
+
title: 'Async function without try/catch at boundary',
|
|
123
|
+
check({ files }) {
|
|
124
|
+
const findings = [];
|
|
125
|
+
for (const [fp, content] of files) {
|
|
126
|
+
if (!isJS(fp)) continue;
|
|
127
|
+
if (!fp.match(/(?:route|controller|handler|api|endpoint|middleware)/i)) continue;
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
if (/(?:async\s+(?:function\s+\w+|(?:get|post|put|delete|patch)\s*\())|(?:=\s*async\s*\()/.test(lines[i])) {
|
|
131
|
+
const body = lines.slice(i, Math.min(lines.length, i + 30)).join('\n');
|
|
132
|
+
if (!body.includes('try') && !body.includes('catch') && !body.includes('errorHandler') && body.includes('await')) {
|
|
133
|
+
findings.push({
|
|
134
|
+
ruleId: 'BUG-ERR-005', category: 'bugs', severity: 'high',
|
|
135
|
+
title: 'Async handler without try/catch — unhandled rejection crashes server',
|
|
136
|
+
description: 'Async route handlers need try/catch around awaits. Without it, a rejected promise crashes the server.',
|
|
137
|
+
file: fp, line: i + 1, fix: null,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return findings;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
export default rules;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Bug detection: JavaScript logic and control flow bugs
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-LOGIC-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'switch fallthrough — missing break statement',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
if (/^\s*case\s+/.test(lines[i])) {
|
|
19
|
+
// Look ahead for break/return before next case/default
|
|
20
|
+
let hasBreak = false;
|
|
21
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 20); j++) {
|
|
22
|
+
if (/^\s*(break|return|throw|continue)\b/.test(lines[j])) { hasBreak = true; break; }
|
|
23
|
+
if (/^\s*case\s+/.test(lines[j]) || /^\s*default\s*:/.test(lines[j])) {
|
|
24
|
+
// Check for intentional fallthrough comment
|
|
25
|
+
const between = lines.slice(i, j).join('\n');
|
|
26
|
+
if (!/fall\s*through|intentional|eslint-disable/i.test(between) && between.trim().split('\n').length > 1) {
|
|
27
|
+
findings.push({
|
|
28
|
+
ruleId: 'BUG-LOGIC-001', category: 'bugs', severity: 'high',
|
|
29
|
+
title: 'switch case fallthrough — missing break/return',
|
|
30
|
+
description: 'This case falls through to the next one without break or return. Add break; or a // fallthrough comment if intentional.',
|
|
31
|
+
file: fp, line: i + 1, fix: null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (/^\s*\}\s*$/.test(lines[j])) break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return findings;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'BUG-LOGIC-002',
|
|
46
|
+
category: 'bugs',
|
|
47
|
+
severity: 'high',
|
|
48
|
+
confidence: 'likely',
|
|
49
|
+
title: 'Empty catch block — errors silently swallowed',
|
|
50
|
+
check({ files }) {
|
|
51
|
+
const findings = [];
|
|
52
|
+
for (const [fp, content] of files) {
|
|
53
|
+
if (!isJS(fp)) continue;
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
if (/\bcatch\s*\(/.test(lines[i]) || /\bcatch\s*\{/.test(lines[i])) {
|
|
57
|
+
// Check if catch body is empty
|
|
58
|
+
const next3 = lines.slice(i, i + 4).join('\n');
|
|
59
|
+
if (/catch\s*(?:\(\s*\w*\s*\))?\s*\{\s*\}/.test(next3)) {
|
|
60
|
+
findings.push({
|
|
61
|
+
ruleId: 'BUG-LOGIC-002', category: 'bugs', severity: 'high',
|
|
62
|
+
title: 'Empty catch block — errors silently swallowed',
|
|
63
|
+
description: 'Catching errors without logging or handling them hides bugs. At minimum, log the error.',
|
|
64
|
+
file: fp, line: i + 1, fix: null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return findings;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'BUG-LOGIC-003',
|
|
75
|
+
category: 'bugs',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
confidence: 'likely',
|
|
78
|
+
title: 'Assignment in conditional — likely meant comparison',
|
|
79
|
+
check({ files }) {
|
|
80
|
+
const findings = [];
|
|
81
|
+
for (const [fp, content] of files) {
|
|
82
|
+
if (!isJS(fp)) continue;
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
// if (x = something) — likely meant ===
|
|
87
|
+
if (/\bif\s*\((?:.*[^!=<>])([^!=<>])=([^=])/.test(line)) {
|
|
88
|
+
const match = line.match(/\bif\s*\(\s*(\w+)\s*=\s*(?!=)/);
|
|
89
|
+
if (match && !line.includes('==') && !line.includes('===')) {
|
|
90
|
+
findings.push({
|
|
91
|
+
ruleId: 'BUG-LOGIC-003', category: 'bugs', severity: 'high',
|
|
92
|
+
title: 'Assignment (=) in if condition — did you mean === ?',
|
|
93
|
+
description: 'if (x = value) assigns value to x and evaluates. This is almost always a bug — use === for comparison.',
|
|
94
|
+
file: fp, line: i + 1, fix: null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'BUG-LOGIC-004',
|
|
105
|
+
category: 'bugs',
|
|
106
|
+
severity: 'high',
|
|
107
|
+
confidence: 'likely',
|
|
108
|
+
title: 'Negated condition with || instead of && (De Morgan\'s law)',
|
|
109
|
+
check({ files }) {
|
|
110
|
+
const findings = [];
|
|
111
|
+
for (const [fp, content] of files) {
|
|
112
|
+
if (!isJS(fp)) continue;
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i];
|
|
116
|
+
// if (!a || !b) when they likely meant if (!(a && b)) which is if (!a || !b) — actually these are the same
|
|
117
|
+
// More useful: if (!a || b) or if (a || !b) inside a guard clause that should use &&
|
|
118
|
+
// Detect: if (x !== 'a' || x !== 'b') — always true, likely meant &&
|
|
119
|
+
const m = line.match(/(\w+)\s*!==?\s*['"](\w+)['"]\s*\|\|\s*\1\s*!==?\s*['"]/);
|
|
120
|
+
if (m) {
|
|
121
|
+
findings.push({
|
|
122
|
+
ruleId: 'BUG-LOGIC-004', category: 'bugs', severity: 'high',
|
|
123
|
+
title: 'Condition is always true — x !== "a" || x !== "b" is always true',
|
|
124
|
+
description: 'If x is "a", the second check passes. If x is "b", the first check passes. You likely meant && (AND) not || (OR).',
|
|
125
|
+
file: fp, line: i + 1, fix: null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return findings;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'BUG-LOGIC-005',
|
|
135
|
+
category: 'bugs',
|
|
136
|
+
severity: 'high',
|
|
137
|
+
confidence: 'likely',
|
|
138
|
+
title: 'Unreachable code after return/throw',
|
|
139
|
+
check({ files }) {
|
|
140
|
+
const findings = [];
|
|
141
|
+
for (const [fp, content] of files) {
|
|
142
|
+
if (!isJS(fp)) continue;
|
|
143
|
+
const lines = content.split('\n');
|
|
144
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
145
|
+
const line = lines[i];
|
|
146
|
+
// Must be a standalone return/throw statement (not inside ternary, not multi-line return)
|
|
147
|
+
if (!/^\s+(return|throw)\s+/.test(line)) continue;
|
|
148
|
+
if (/\bif\b|\belse\b|\?|&&|\|\|/.test(line)) continue;
|
|
149
|
+
// Skip multi-line returns (line doesn't end with ; or contains opening brace/paren/template)
|
|
150
|
+
if (/[({`+,]$/.test(line.trim()) || !/[;)]?\s*$/.test(line.trim())) continue;
|
|
151
|
+
const nextLine = lines[i + 1];
|
|
152
|
+
if (!nextLine) continue;
|
|
153
|
+
const trimmed = nextLine.trim();
|
|
154
|
+
// Skip if next line is: closing brace, comment, blank, case/default, label, else
|
|
155
|
+
if (!trimmed || /^[}\])]|^\/\/|^\/\*|^\*|^case\b|^default\b|^else\b|^break\b/.test(trimmed)) continue;
|
|
156
|
+
// Skip if it's the continuation of an object/array/template literal
|
|
157
|
+
if (/^[.?+\-*,|&]|^\}|^`/.test(trimmed)) continue;
|
|
158
|
+
// Must be actual code (starts with word char or specific statement starters)
|
|
159
|
+
if (!/^[a-zA-Z_$]/.test(trimmed)) continue;
|
|
160
|
+
findings.push({
|
|
161
|
+
ruleId: 'BUG-LOGIC-005', category: 'bugs', severity: 'high',
|
|
162
|
+
title: 'Unreachable code after return/throw',
|
|
163
|
+
description: 'Code after return or throw is never executed. Move it before the return or remove it.',
|
|
164
|
+
file: fp, line: i + 2, fix: null,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'BUG-LOGIC-006',
|
|
173
|
+
category: 'bugs',
|
|
174
|
+
severity: 'high',
|
|
175
|
+
confidence: 'likely',
|
|
176
|
+
title: 'Constant condition in loop or if — dead code or infinite loop',
|
|
177
|
+
check({ files }) {
|
|
178
|
+
const findings = [];
|
|
179
|
+
for (const [fp, content] of files) {
|
|
180
|
+
if (!isJS(fp)) continue;
|
|
181
|
+
const lines = content.split('\n');
|
|
182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
183
|
+
const line = lines[i];
|
|
184
|
+
if (/\bif\s*\(\s*(true|false|1|0)\s*\)/.test(line)) {
|
|
185
|
+
findings.push({
|
|
186
|
+
ruleId: 'BUG-LOGIC-006', category: 'bugs', severity: 'high',
|
|
187
|
+
title: 'Constant condition — branch is always taken or never taken',
|
|
188
|
+
description: 'if(true) or if(false) is dead code. Remove the condition or the dead branch.',
|
|
189
|
+
file: fp, line: i + 1, fix: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return findings;
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'BUG-LOGIC-007',
|
|
199
|
+
category: 'bugs',
|
|
200
|
+
severity: 'high',
|
|
201
|
+
confidence: 'likely',
|
|
202
|
+
title: 'Duplicate key in object literal — second value overwrites first',
|
|
203
|
+
check({ files }) {
|
|
204
|
+
const findings = [];
|
|
205
|
+
for (const [fp, content] of files) {
|
|
206
|
+
if (!isJS(fp)) continue;
|
|
207
|
+
// Find duplicate keys in object literals
|
|
208
|
+
const objBlockRegex = /\{([^{}]{10,500})\}/g;
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = objBlockRegex.exec(content)) !== null) {
|
|
211
|
+
const block = match[1];
|
|
212
|
+
const keyRegex = /(?:^|,|\n)\s*['"]?(\w+)['"]?\s*:/g;
|
|
213
|
+
const keys = {};
|
|
214
|
+
let keyMatch;
|
|
215
|
+
while ((keyMatch = keyRegex.exec(block)) !== null) {
|
|
216
|
+
const key = keyMatch[1];
|
|
217
|
+
if (keys[key]) {
|
|
218
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
219
|
+
findings.push({
|
|
220
|
+
ruleId: 'BUG-LOGIC-007', category: 'bugs', severity: 'high',
|
|
221
|
+
title: `Duplicate key "${key}" in object literal — second value overwrites first`,
|
|
222
|
+
description: 'JavaScript silently uses the last value for duplicate keys. The first assignment is lost.',
|
|
223
|
+
file: fp, line: lineNum, fix: null,
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
keys[key] = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'BUG-LOGIC-008',
|
|
236
|
+
category: 'bugs',
|
|
237
|
+
severity: 'high',
|
|
238
|
+
confidence: 'likely',
|
|
239
|
+
title: 'Semicolon after if/for — body never executes conditionally',
|
|
240
|
+
check({ files }) {
|
|
241
|
+
const findings = [];
|
|
242
|
+
for (const [fp, content] of files) {
|
|
243
|
+
if (!isJS(fp)) continue;
|
|
244
|
+
const lines = content.split('\n');
|
|
245
|
+
for (let i = 0; i < lines.length; i++) {
|
|
246
|
+
if (/\b(if|for|while)\s*\([^)]+\)\s*;/.test(lines[i]) && !/for\s*\([^)]*;\s*[^)]*;\s*[^)]*\)/.test(lines[i])) {
|
|
247
|
+
findings.push({
|
|
248
|
+
ruleId: 'BUG-LOGIC-008', category: 'bugs', severity: 'high',
|
|
249
|
+
title: 'Semicolon after if/for — body executes unconditionally',
|
|
250
|
+
description: 'if (condition); { body } — the semicolon ends the if statement, so the body always executes. Remove the semicolon.',
|
|
251
|
+
file: fp, line: i + 1, fix: null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
export default rules;
|