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,365 @@
|
|
|
1
|
+
// Bug detection: Patterns that AI code generators (Claude, GPT, Copilot) commonly produce wrong
|
|
2
|
+
// These are the "looks right, works in demos, breaks in production" patterns.
|
|
3
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
4
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
5
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
6
|
+
|
|
7
|
+
const rules = [
|
|
8
|
+
{
|
|
9
|
+
id: 'BUG-AI-001',
|
|
10
|
+
category: 'bugs',
|
|
11
|
+
severity: 'high',
|
|
12
|
+
confidence: 'likely',
|
|
13
|
+
title: 'Placeholder error handling — catch block only logs',
|
|
14
|
+
description: 'AI often generates catch blocks that just console.log the error. In production, errors should be properly handled.',
|
|
15
|
+
check({ files }) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const [fp, content] of files) {
|
|
18
|
+
if (!isJS(fp)) continue;
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
if (/\}\s*catch\s*\(\s*\w+\s*\)\s*\{/.test(lines[i]) || /catch\s*\(\s*\w+\s*\)\s*\{/.test(lines[i])) {
|
|
22
|
+
// Check if the catch block only has console.log/error and nothing else
|
|
23
|
+
let catchBody = '';
|
|
24
|
+
let braceDepth = 0;
|
|
25
|
+
let started = false;
|
|
26
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
27
|
+
if (lines[j].includes('{')) { started = true; braceDepth += (lines[j].match(/\{/g) || []).length; }
|
|
28
|
+
if (started) {
|
|
29
|
+
catchBody += lines[j] + '\n';
|
|
30
|
+
braceDepth -= (lines[j].match(/\}/g) || []).length;
|
|
31
|
+
if (braceDepth <= 0) break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const bodyLines = catchBody.split('\n').map(l => l.trim()).filter(l => l && l !== '{' && l !== '}' && !l.startsWith('//'));
|
|
35
|
+
if (bodyLines.length <= 2 && /console\.(log|error|warn)\s*\(/.test(catchBody) && !/throw\b|return\b|reject\b|next\s*\(|res\.\w+\(|process\.exit/.test(catchBody)) {
|
|
36
|
+
findings.push({
|
|
37
|
+
ruleId: 'BUG-AI-001', category: 'bugs', severity: 'high',
|
|
38
|
+
title: 'Catch block only logs error — no recovery, rethrow, or user feedback',
|
|
39
|
+
description: 'AI-generated catch blocks often just log and swallow errors. Add proper error handling: rethrow, return error response, or graceful degradation.',
|
|
40
|
+
file: fp, line: i + 1, fix: null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'BUG-AI-002',
|
|
51
|
+
category: 'bugs',
|
|
52
|
+
severity: 'high',
|
|
53
|
+
confidence: 'likely',
|
|
54
|
+
title: 'Hardcoded localhost/port in non-config file',
|
|
55
|
+
check({ files }) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const [fp, content] of files) {
|
|
58
|
+
if (!isJS(fp) && !isPy(fp)) continue;
|
|
59
|
+
if (/config|\.env|constant|setting/i.test(fp)) continue;
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
for (let i = 0; i < lines.length; i++) {
|
|
62
|
+
if (/['"`]https?:\/\/localhost:\d+/.test(lines[i]) || /['"`]https?:\/\/127\.0\.0\.1:\d+/.test(lines[i])) {
|
|
63
|
+
if (!/\/\/\s*(test|dev|example|TODO|FIXME)/.test(lines[i]) && !/\.test\.|\.spec\.|__test__/.test(fp)) {
|
|
64
|
+
findings.push({
|
|
65
|
+
ruleId: 'BUG-AI-002', category: 'bugs', severity: 'high',
|
|
66
|
+
title: 'Hardcoded localhost URL in source code',
|
|
67
|
+
description: 'AI generators default to localhost URLs that break in production. Use environment variables or config.',
|
|
68
|
+
file: fp, line: i + 1, fix: null,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'BUG-AI-003',
|
|
79
|
+
category: 'bugs',
|
|
80
|
+
severity: 'medium',
|
|
81
|
+
confidence: 'likely',
|
|
82
|
+
title: 'TODO/placeholder left by AI generation',
|
|
83
|
+
check({ files }) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
for (const [fp, content] of files) {
|
|
86
|
+
if (!isJS(fp) && !isPy(fp)) continue;
|
|
87
|
+
const lines = content.split('\n');
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
// AI-specific placeholder patterns
|
|
91
|
+
if (/\/\/\s*TODO:\s*(implement|add|replace|fix|handle|complete|fill|update)\s+(this|here|later|logic|code|implementation)/i.test(line) ||
|
|
92
|
+
/\/\/\s*\.\.\.\s*(rest|more|other|remaining)\s+(of|code|logic|implementation)/i.test(line) ||
|
|
93
|
+
/#\s*TODO:\s*(implement|add|replace|fix|handle|complete|fill)\s+(this|here|later|logic)/i.test(line) ||
|
|
94
|
+
/['"`]your[-_]?(api[-_]?key|secret|token|password|database[-_]?url)['"`]/i.test(line) ||
|
|
95
|
+
/['"`]sk-[\.x]+['"`]/.test(line) ||
|
|
96
|
+
/['"`]placeholder['"`]/i.test(line) && /=/.test(line)) {
|
|
97
|
+
findings.push({
|
|
98
|
+
ruleId: 'BUG-AI-003', category: 'bugs', severity: 'medium',
|
|
99
|
+
title: 'AI-generated placeholder/TODO not implemented',
|
|
100
|
+
description: 'AI code generators leave TODOs and placeholder values that must be replaced before production use.',
|
|
101
|
+
file: fp, line: i + 1, fix: null,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'BUG-AI-004',
|
|
111
|
+
category: 'bugs',
|
|
112
|
+
severity: 'high',
|
|
113
|
+
confidence: 'likely',
|
|
114
|
+
title: 'Optimistic data fetching — no loading/error states',
|
|
115
|
+
check({ files }) {
|
|
116
|
+
const findings = [];
|
|
117
|
+
for (const [fp, content] of files) {
|
|
118
|
+
if (!isJS(fp)) continue;
|
|
119
|
+
// Check for React components that fetch data but don't handle loading/error
|
|
120
|
+
if (!/use(State|Effect)/.test(content)) continue;
|
|
121
|
+
const lines = content.split('\n');
|
|
122
|
+
const hasDataFetch = /fetch\(|axios\.|\.get\(|\.post\(|useSWR|useQuery/.test(content);
|
|
123
|
+
const hasLoadingState = /loading|isLoading|pending|isFetching|skeleton|spinner/i.test(content);
|
|
124
|
+
const hasErrorState = /error|isError|errorMessage|onError|\.catch/.test(content);
|
|
125
|
+
if (hasDataFetch && !hasLoadingState && !hasErrorState) {
|
|
126
|
+
findings.push({
|
|
127
|
+
ruleId: 'BUG-AI-004', category: 'bugs', severity: 'high',
|
|
128
|
+
title: 'Data fetching without loading or error states',
|
|
129
|
+
description: 'AI-generated components often fetch data optimistically without handling loading and error states. Users see blank screens or stale data.',
|
|
130
|
+
file: fp, line: 1, fix: null,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return findings;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'BUG-AI-005',
|
|
139
|
+
category: 'bugs',
|
|
140
|
+
severity: 'medium',
|
|
141
|
+
confidence: 'likely',
|
|
142
|
+
title: 'Copy-pasted code with inconsistent variable names',
|
|
143
|
+
check({ files }) {
|
|
144
|
+
const findings = [];
|
|
145
|
+
for (const [fp, content] of files) {
|
|
146
|
+
if (!isJS(fp) && !isPy(fp)) continue;
|
|
147
|
+
const lines = content.split('\n');
|
|
148
|
+
// Find functions/blocks that look very similar (AI often copy-pastes and partially edits)
|
|
149
|
+
const funcBodies = [];
|
|
150
|
+
let currentFunc = null;
|
|
151
|
+
let braceDepth = 0;
|
|
152
|
+
for (let i = 0; i < lines.length; i++) {
|
|
153
|
+
const funcMatch = lines[i].match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\()/);
|
|
154
|
+
if (funcMatch) {
|
|
155
|
+
if (currentFunc && currentFunc.body.length > 3) {
|
|
156
|
+
funcBodies.push(currentFunc);
|
|
157
|
+
}
|
|
158
|
+
currentFunc = { name: funcMatch[1] || funcMatch[2], line: i + 1, body: [] };
|
|
159
|
+
braceDepth = 0;
|
|
160
|
+
}
|
|
161
|
+
if (currentFunc) {
|
|
162
|
+
currentFunc.body.push(lines[i]);
|
|
163
|
+
braceDepth += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length;
|
|
164
|
+
if (braceDepth <= 0 && currentFunc.body.length > 1) {
|
|
165
|
+
funcBodies.push(currentFunc);
|
|
166
|
+
currentFunc = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Compare function bodies for near-duplicates
|
|
171
|
+
for (let a = 0; a < funcBodies.length; a++) {
|
|
172
|
+
for (let b = a + 1; b < funcBodies.length; b++) {
|
|
173
|
+
if (funcBodies[a].body.length < 5) continue;
|
|
174
|
+
const lenDiff = Math.abs(funcBodies[a].body.length - funcBodies[b].body.length);
|
|
175
|
+
if (lenDiff > 3) continue;
|
|
176
|
+
// Normalize: remove variable names, keep structure
|
|
177
|
+
const normalize = (lines) => lines.map(l => l.replace(/\b[a-z]\w*\b/gi, '_').replace(/\s+/g, ' ').trim()).filter(l => l.length > 5).join('\n');
|
|
178
|
+
const na = normalize(funcBodies[a].body);
|
|
179
|
+
const nb = normalize(funcBodies[b].body);
|
|
180
|
+
if (na === nb && na.length > 50) {
|
|
181
|
+
findings.push({
|
|
182
|
+
ruleId: 'BUG-AI-005', category: 'bugs', severity: 'medium',
|
|
183
|
+
title: `Near-duplicate functions: "${funcBodies[a].name}" and "${funcBodies[b].name}"`,
|
|
184
|
+
description: 'AI generators often copy-paste functions with minor changes. This leads to maintenance burden and inconsistent behavior. Consider extracting shared logic.',
|
|
185
|
+
file: fp, line: funcBodies[b].line, fix: null,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return findings;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'BUG-AI-006',
|
|
196
|
+
category: 'bugs',
|
|
197
|
+
severity: 'critical',
|
|
198
|
+
confidence: 'likely',
|
|
199
|
+
title: 'User input used directly in database query',
|
|
200
|
+
check({ files }) {
|
|
201
|
+
const findings = [];
|
|
202
|
+
for (const [fp, content] of files) {
|
|
203
|
+
if (!isJS(fp)) continue;
|
|
204
|
+
const lines = content.split('\n');
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
const line = lines[i];
|
|
207
|
+
// Template literal in query with req.body/req.params/req.query
|
|
208
|
+
if (/`.*\$\{.*req\.(body|params|query|headers)\b/.test(line) && /\b(query|execute|raw|sql)\s*\(/.test(line)) {
|
|
209
|
+
findings.push({
|
|
210
|
+
ruleId: 'BUG-AI-006', category: 'bugs', severity: 'high',
|
|
211
|
+
title: 'SQL injection — user input interpolated into query string',
|
|
212
|
+
description: 'AI generators often use template literals for SQL queries with user input. Use parameterized queries instead.',
|
|
213
|
+
file: fp, line: i + 1, fix: null,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// String concatenation in query
|
|
217
|
+
if (/\b(query|execute)\s*\(\s*['"`].*\+\s*req\.(body|params|query)/.test(line)) {
|
|
218
|
+
findings.push({
|
|
219
|
+
ruleId: 'BUG-AI-006', category: 'bugs', severity: 'high',
|
|
220
|
+
title: 'SQL injection — user input concatenated into query string',
|
|
221
|
+
description: 'Never concatenate user input into SQL queries. Use parameterized queries ($1, ?, :param).',
|
|
222
|
+
file: fp, line: i + 1, fix: null,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return findings;
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'BUG-AI-007',
|
|
232
|
+
category: 'bugs',
|
|
233
|
+
severity: 'medium',
|
|
234
|
+
confidence: 'likely',
|
|
235
|
+
title: 'Inconsistent error response format',
|
|
236
|
+
check({ files }) {
|
|
237
|
+
const findings = [];
|
|
238
|
+
for (const [fp, content] of files) {
|
|
239
|
+
if (!isJS(fp)) continue;
|
|
240
|
+
if (!/express|fastify|koa|router/.test(content)) continue;
|
|
241
|
+
const lines = content.split('\n');
|
|
242
|
+
const errorFormats = new Set();
|
|
243
|
+
for (let i = 0; i < lines.length; i++) {
|
|
244
|
+
// Detect various error response patterns
|
|
245
|
+
if (/res\.\w*status\s*\(\s*(4|5)\d\d\s*\)/.test(lines[i])) {
|
|
246
|
+
const block = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
|
|
247
|
+
if (/\{\s*error\s*:/.test(block)) errorFormats.add('error');
|
|
248
|
+
else if (/\{\s*message\s*:/.test(block)) errorFormats.add('message');
|
|
249
|
+
else if (/\{\s*msg\s*:/.test(block)) errorFormats.add('msg');
|
|
250
|
+
else if (/\{\s*err\s*:/.test(block)) errorFormats.add('err');
|
|
251
|
+
else if (/\.send\s*\(\s*['"`]/.test(block)) errorFormats.add('string');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (errorFormats.size > 2) {
|
|
255
|
+
findings.push({
|
|
256
|
+
ruleId: 'BUG-AI-007', category: 'bugs', severity: 'medium',
|
|
257
|
+
title: `${errorFormats.size} different error response formats (${[...errorFormats].join(', ')})`,
|
|
258
|
+
description: 'AI generates different error shapes in each handler. Clients can\'t reliably parse errors. Standardize on one format like { error: { message, code } }.',
|
|
259
|
+
file: fp, line: 1, fix: null,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'BUG-AI-008',
|
|
268
|
+
category: 'bugs',
|
|
269
|
+
severity: 'high',
|
|
270
|
+
confidence: 'likely',
|
|
271
|
+
title: 'No input validation on API endpoint',
|
|
272
|
+
check({ files }) {
|
|
273
|
+
const findings = [];
|
|
274
|
+
for (const [fp, content] of files) {
|
|
275
|
+
if (!isJS(fp)) continue;
|
|
276
|
+
const lines = content.split('\n');
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
// POST/PUT/PATCH handler
|
|
279
|
+
if (/\.(post|put|patch)\s*\(\s*['"`]/.test(lines[i])) {
|
|
280
|
+
let block = '';
|
|
281
|
+
for (let j = i; j < Math.min(i + 25, lines.length); j++) {
|
|
282
|
+
block += lines[j] + '\n';
|
|
283
|
+
}
|
|
284
|
+
// Uses req.body but no validation
|
|
285
|
+
if (/req\.body/.test(block) && !/validate|schema|joi|zod|yup|ajv|express-validator|check\s*\(|body\s*\(/.test(block)) {
|
|
286
|
+
// Check for at least basic checks
|
|
287
|
+
if (!/if\s*\(\s*!req\.body|typeof\s+req\.body|req\.body\.\w+\s*===|!req\.body\.\w+/.test(block)) {
|
|
288
|
+
findings.push({
|
|
289
|
+
ruleId: 'BUG-AI-008', category: 'bugs', severity: 'high',
|
|
290
|
+
title: 'API endpoint accepts req.body without any input validation',
|
|
291
|
+
description: 'AI-generated endpoints often trust req.body blindly. Add validation (zod, joi, or manual checks) to prevent crashes and security issues.',
|
|
292
|
+
file: fp, line: i + 1, fix: null,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return findings;
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'BUG-AI-009',
|
|
304
|
+
category: 'bugs',
|
|
305
|
+
severity: 'medium',
|
|
306
|
+
confidence: 'likely',
|
|
307
|
+
title: 'Unused import or variable from AI generation',
|
|
308
|
+
check({ files }) {
|
|
309
|
+
const findings = [];
|
|
310
|
+
for (const [fp, content] of files) {
|
|
311
|
+
if (!isJS(fp)) continue;
|
|
312
|
+
const lines = content.split('\n');
|
|
313
|
+
// Find simple imports: import X from 'y' or const X = require('y')
|
|
314
|
+
for (let i = 0; i < lines.length; i++) {
|
|
315
|
+
let importName = null;
|
|
316
|
+
const defaultImport = lines[i].match(/^import\s+(\w+)\s+from\s+/);
|
|
317
|
+
const requireImport = lines[i].match(/^(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(/);
|
|
318
|
+
if (defaultImport) importName = defaultImport[1];
|
|
319
|
+
else if (requireImport) importName = requireImport[1];
|
|
320
|
+
if (!importName) continue;
|
|
321
|
+
// Check if the import is used in the rest of the file
|
|
322
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
323
|
+
const usageRe = new RegExp(`\\b${importName}\\b`);
|
|
324
|
+
if (!usageRe.test(rest)) {
|
|
325
|
+
findings.push({
|
|
326
|
+
ruleId: 'BUG-AI-009', category: 'bugs', severity: 'medium',
|
|
327
|
+
title: `Unused import: "${importName}" imported but never used`,
|
|
328
|
+
description: 'AI generators often add imports they don\'t end up using. Dead imports bloat bundles and cause confusion.',
|
|
329
|
+
file: fp, line: i + 1, fix: null,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return findings;
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: 'BUG-AI-010',
|
|
339
|
+
category: 'bugs',
|
|
340
|
+
severity: 'high',
|
|
341
|
+
confidence: 'likely',
|
|
342
|
+
title: 'Missing CORS configuration in API server',
|
|
343
|
+
check({ files }) {
|
|
344
|
+
const findings = [];
|
|
345
|
+
for (const [fp, content] of files) {
|
|
346
|
+
if (!isJS(fp)) continue;
|
|
347
|
+
// Is this a server entry point?
|
|
348
|
+
if (!/express\(\)|createServer|fastify\(\)|new Koa/.test(content)) continue;
|
|
349
|
+
if (!/\.(get|post|put|delete)\s*\(/.test(content)) continue;
|
|
350
|
+
// Check for CORS setup
|
|
351
|
+
if (!/cors\(|Access-Control-Allow|\.use\(\s*cors/.test(content)) {
|
|
352
|
+
findings.push({
|
|
353
|
+
ruleId: 'BUG-AI-010', category: 'bugs', severity: 'high',
|
|
354
|
+
title: 'API server without CORS configuration — frontend requests will fail',
|
|
355
|
+
description: 'AI often generates separate frontend and backend but forgets CORS. Browsers block cross-origin requests without proper headers.',
|
|
356
|
+
file: fp, line: 1, fix: null,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return findings;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
export default rules;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Bug detection: Code smell patterns — off-by-one, path confusion, counter bugs
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
const PY_EXT = ['.py'];
|
|
4
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
5
|
+
function isPy(f) { return PY_EXT.some(e => f.endsWith(e)); }
|
|
6
|
+
function isSource(f) {
|
|
7
|
+
return ['.js','.jsx','.ts','.tsx','.mjs','.cjs','.py','.go','.java','.cs','.rb','.php','.rs','.swift','.dart'].some(e => f.endsWith(e));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const rules = [
|
|
11
|
+
// BUG-SMELL-001: Hardcoded slice index on dynamic string (off-by-one risk)
|
|
12
|
+
{
|
|
13
|
+
id: 'BUG-SMELL-001',
|
|
14
|
+
category: 'bugs',
|
|
15
|
+
severity: 'medium',
|
|
16
|
+
confidence: 'suggestion',
|
|
17
|
+
title: 'Hardcoded .slice() offset on dynamic string — off-by-one risk',
|
|
18
|
+
check({ files }) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const [fp, content] of files) {
|
|
21
|
+
if (!isJS(fp)) continue;
|
|
22
|
+
const lines = content.split('\n');
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
if (line.trim().startsWith('//')) continue;
|
|
26
|
+
// Pattern: variable.slice(hardcoded_number, ...) where the number > 1
|
|
27
|
+
// Suggests the offset was calculated by hand instead of dynamically
|
|
28
|
+
const match = line.match(/(\w+)\.slice\(\s*(\d+)\s*,/);
|
|
29
|
+
if (match) {
|
|
30
|
+
const offset = parseInt(match[2], 10);
|
|
31
|
+
// Only flag larger hardcoded offsets that look like manual string math
|
|
32
|
+
if (offset >= 4 && !/\.slice\(\s*\d+\s*,\s*-?\d+\s*\)/.test(line)) {
|
|
33
|
+
// Skip if the variable is a known literal context like a regex match or array
|
|
34
|
+
if (/match|split|entries|keys|values|args|argv/.test(match[1])) continue;
|
|
35
|
+
findings.push({
|
|
36
|
+
ruleId: 'BUG-SMELL-001', category: 'bugs', severity: 'medium',
|
|
37
|
+
title: `Hardcoded .slice(${offset}) — use indexOf/dynamic offset instead`,
|
|
38
|
+
description: `Hardcoding slice offset ${offset} is fragile. If the string format changes, the offset will be wrong. Use .indexOf() or .search() to find the position dynamically.`,
|
|
39
|
+
file: fp, line: i + 1,
|
|
40
|
+
fix: null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// BUG-SMELL-002: Counter incremented twice in loop (double-increment bug)
|
|
51
|
+
{
|
|
52
|
+
id: 'BUG-SMELL-002',
|
|
53
|
+
category: 'bugs',
|
|
54
|
+
severity: 'high',
|
|
55
|
+
confidence: 'likely',
|
|
56
|
+
title: 'Counter variable incremented twice — possible double-increment bug',
|
|
57
|
+
check({ files }) {
|
|
58
|
+
const findings = [];
|
|
59
|
+
for (const [fp, content] of files) {
|
|
60
|
+
if (!isJS(fp)) continue;
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
if (line.trim().startsWith('//')) continue;
|
|
65
|
+
// Find counter++ or counter += 1
|
|
66
|
+
const incMatch = line.match(/\b(\w+)\s*(?:\+\+|\+=\s*1)/);
|
|
67
|
+
if (!incMatch) continue;
|
|
68
|
+
const varName = incMatch[1];
|
|
69
|
+
// Check if same counter is incremented again within the next 10 lines
|
|
70
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
|
|
71
|
+
const nextLine = lines[j];
|
|
72
|
+
if (nextLine.trim().startsWith('//')) continue;
|
|
73
|
+
// Check for same variable incremented again
|
|
74
|
+
const re = new RegExp(`\\b${varName}\\s*(?:\\+\\+|\\+=\\s*1)`);
|
|
75
|
+
if (re.test(nextLine)) {
|
|
76
|
+
// Check if there's a conditional (if/else) between them — that's OK
|
|
77
|
+
const between = lines.slice(i + 1, j).join(' ');
|
|
78
|
+
if (/\bif\s*\(/.test(between) || /\belse\b/.test(between) || /\bcontinue\b/.test(between)) break;
|
|
79
|
+
findings.push({
|
|
80
|
+
ruleId: 'BUG-SMELL-002', category: 'bugs', severity: 'high',
|
|
81
|
+
title: `"${varName}" incremented twice without condition — double-increment bug`,
|
|
82
|
+
description: `Counter "${varName}" is incremented on line ${i + 1} and again on line ${j + 1} without any condition. This usually means the counter advances faster than intended.`,
|
|
83
|
+
file: fp, line: i + 1,
|
|
84
|
+
fix: null,
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// BUG-SMELL-003: File path used as directory (join(filePath, ...))
|
|
96
|
+
{
|
|
97
|
+
id: 'BUG-SMELL-003',
|
|
98
|
+
category: 'bugs',
|
|
99
|
+
severity: 'high',
|
|
100
|
+
confidence: 'likely',
|
|
101
|
+
title: 'File path used as directory in path.join() — ENOTDIR crash risk',
|
|
102
|
+
check({ files }) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
for (const [fp, content] of files) {
|
|
105
|
+
if (!isJS(fp)) continue;
|
|
106
|
+
const lines = content.split('\n');
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
const line = lines[i];
|
|
109
|
+
if (line.trim().startsWith('//')) continue;
|
|
110
|
+
// Pattern: join(somePath, 'filename') where somePath could be a file
|
|
111
|
+
// Check if the function validates the path is a directory first
|
|
112
|
+
const joinMatch = line.match(/\bjoin\s*\(\s*(\w+)\s*,\s*['"]([^'"]+)['"]\s*\)/);
|
|
113
|
+
if (!joinMatch) continue;
|
|
114
|
+
const [, pathVar, filename] = joinMatch;
|
|
115
|
+
// Only flag if the filename looks like a config/cache file
|
|
116
|
+
if (!/\.(json|yml|yaml|toml|lock|log|cache|tmp|config)$/.test(filename)) continue;
|
|
117
|
+
// Check if there's a statSync/isDirectory check nearby
|
|
118
|
+
const context = lines.slice(Math.max(0, i - 5), i).join(' ');
|
|
119
|
+
if (/isDirectory|statSync|lstatSync/.test(context)) continue;
|
|
120
|
+
findings.push({
|
|
121
|
+
ruleId: 'BUG-SMELL-003', category: 'bugs', severity: 'high',
|
|
122
|
+
title: `path.join(${pathVar}, "${filename}") — crashes if ${pathVar} is a file, not a directory`,
|
|
123
|
+
description: `If "${pathVar}" is a file path instead of a directory, join() creates an invalid path (e.g., /file.js/${filename}). Add a check: statSync(${pathVar}).isDirectory() or use dirname(${pathVar}).`,
|
|
124
|
+
file: fp, line: i + 1,
|
|
125
|
+
fix: null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return findings;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// BUG-SMELL-004: String split result assumed to have N elements
|
|
134
|
+
{
|
|
135
|
+
id: 'BUG-SMELL-004',
|
|
136
|
+
category: 'bugs',
|
|
137
|
+
severity: 'medium',
|
|
138
|
+
confidence: 'suggestion',
|
|
139
|
+
title: 'Accessing split() result by index without length check',
|
|
140
|
+
check({ files }) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
for (const [fp, content] of files) {
|
|
143
|
+
if (!isJS(fp)) continue;
|
|
144
|
+
const lines = content.split('\n');
|
|
145
|
+
for (let i = 0; i < lines.length; i++) {
|
|
146
|
+
const line = lines[i];
|
|
147
|
+
if (line.trim().startsWith('//')) continue;
|
|
148
|
+
// Pattern: str.split(x)[N] where N >= 1
|
|
149
|
+
const match = line.match(/\.split\s*\([^)]+\)\s*\[\s*(\d+)\s*\]/);
|
|
150
|
+
if (match) {
|
|
151
|
+
const idx = parseInt(match[1], 10);
|
|
152
|
+
if (idx >= 2) {
|
|
153
|
+
findings.push({
|
|
154
|
+
ruleId: 'BUG-SMELL-004', category: 'bugs', severity: 'medium',
|
|
155
|
+
title: `.split()[${idx}] without length check — undefined if not enough parts`,
|
|
156
|
+
description: `Accessing index ${idx} of split() result returns undefined if the string has fewer delimiters than expected. Add a length check or use a default: .split(x)[${idx}] ?? ''`,
|
|
157
|
+
file: fp, line: i + 1,
|
|
158
|
+
fix: null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return findings;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// BUG-SMELL-005: Regex constructed from user input without escaping
|
|
169
|
+
{
|
|
170
|
+
id: 'BUG-SMELL-005',
|
|
171
|
+
category: 'bugs',
|
|
172
|
+
severity: 'high',
|
|
173
|
+
confidence: 'likely',
|
|
174
|
+
title: 'RegExp constructed from variable without escaping — ReDoS and crash risk',
|
|
175
|
+
check({ files }) {
|
|
176
|
+
const findings = [];
|
|
177
|
+
for (const [fp, content] of files) {
|
|
178
|
+
if (!isJS(fp)) continue;
|
|
179
|
+
const lines = content.split('\n');
|
|
180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
181
|
+
const line = lines[i];
|
|
182
|
+
if (line.trim().startsWith('//')) continue;
|
|
183
|
+
// new RegExp(variable) without escaping
|
|
184
|
+
const match = line.match(/new\s+RegExp\s*\(\s*(\w+)/);
|
|
185
|
+
if (match) {
|
|
186
|
+
const varName = match[1];
|
|
187
|
+
// Skip if it's a string literal
|
|
188
|
+
if (/^['"`]/.test(varName)) continue;
|
|
189
|
+
// Check context for escaping
|
|
190
|
+
const context = lines.slice(Math.max(0, i - 3), i + 1).join(' ');
|
|
191
|
+
if (/escape|sanitize|replace\s*\(\s*\/\[/.test(context)) continue;
|
|
192
|
+
findings.push({
|
|
193
|
+
ruleId: 'BUG-SMELL-005', category: 'bugs', severity: 'high',
|
|
194
|
+
title: `new RegExp(${varName}) — unescaped input can crash or cause ReDoS`,
|
|
195
|
+
description: 'Building a regex from "' + varName + '" without escaping special characters can cause SyntaxError crashes or ReDoS attacks. Escape special chars before constructing the RegExp.',
|
|
196
|
+
file: fp, line: i + 1,
|
|
197
|
+
fix: null,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return findings;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// BUG-SMELL-006: Circular reference in object (extends: self-name)
|
|
207
|
+
{
|
|
208
|
+
id: 'BUG-SMELL-006',
|
|
209
|
+
category: 'bugs',
|
|
210
|
+
severity: 'medium',
|
|
211
|
+
confidence: 'likely',
|
|
212
|
+
title: 'Object contains self-referencing extends/inherits property',
|
|
213
|
+
check({ files }) {
|
|
214
|
+
const findings = [];
|
|
215
|
+
for (const [fp, content] of files) {
|
|
216
|
+
if (!isSource(fp)) continue;
|
|
217
|
+
const lines = content.split('\n');
|
|
218
|
+
// Track current object name
|
|
219
|
+
let currentObj = null;
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
const line = lines[i];
|
|
222
|
+
// Track const/let/var objectName = {
|
|
223
|
+
const objMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*\{/);
|
|
224
|
+
if (objMatch) currentObj = objMatch[1];
|
|
225
|
+
// Check for extends/inherits pointing to same name
|
|
226
|
+
if (currentObj) {
|
|
227
|
+
const extendsMatch = line.match(/extends\s*:\s*['"](\w+)['"]/);
|
|
228
|
+
if (extendsMatch && extendsMatch[1] === currentObj) {
|
|
229
|
+
findings.push({
|
|
230
|
+
ruleId: 'BUG-SMELL-006', category: 'bugs', severity: 'medium',
|
|
231
|
+
title: `Object "${currentObj}" extends itself — circular reference`,
|
|
232
|
+
description: `The "${currentObj}" object has extends: '${currentObj}' which creates a circular reference. This will cause infinite loops if extends is resolved recursively.`,
|
|
233
|
+
file: fp, line: i + 1,
|
|
234
|
+
fix: null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Reset on closing brace at depth 0
|
|
239
|
+
if (/^\};?\s*$/.test(line)) currentObj = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return findings;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
export default rules;
|