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,245 @@
|
|
|
1
|
+
// Bug detection: Advanced Python patterns AI generators get wrong
|
|
2
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
3
|
+
|
|
4
|
+
const rules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'BUG-PY-ADV-001',
|
|
7
|
+
category: 'bugs',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
confidence: 'likely',
|
|
10
|
+
title: 'Flask/Django route without input validation',
|
|
11
|
+
check({ files }) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const [fp, content] of files) {
|
|
14
|
+
if (!isPy(fp)) continue;
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
// @app.route or @router with POST/PUT/PATCH
|
|
18
|
+
if (/@(?:app|router|api)\.(route|post|put|patch)\s*\(/.test(lines[i]) || /methods\s*=\s*\[.*(?:POST|PUT|PATCH)/.test(lines[i])) {
|
|
19
|
+
let block = '';
|
|
20
|
+
for (let j = i; j < Math.min(i + 25, lines.length); j++) {
|
|
21
|
+
block += lines[j] + '\n';
|
|
22
|
+
}
|
|
23
|
+
if (/request\.(json|form|data|get_json|values)/.test(block) && !/validate|schema|serializer|pydantic|marshmallow|wtforms|cerberus|voluptuous/.test(block)) {
|
|
24
|
+
if (!/if\s+not\s+request|if\s+.*not\s+in\s+request|isinstance/.test(block)) {
|
|
25
|
+
findings.push({
|
|
26
|
+
ruleId: 'BUG-PY-ADV-001', category: 'bugs', severity: 'high',
|
|
27
|
+
title: 'Route accepts user input without validation',
|
|
28
|
+
description: 'AI-generated Flask/Django routes often use request.json directly without validation. Use pydantic, marshmallow, or manual checks.',
|
|
29
|
+
file: fp, line: i + 1, fix: null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'BUG-PY-ADV-002',
|
|
41
|
+
category: 'bugs',
|
|
42
|
+
severity: 'high',
|
|
43
|
+
confidence: 'definite',
|
|
44
|
+
title: 'SQL query built with string formatting',
|
|
45
|
+
check({ files }) {
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const [fp, content] of files) {
|
|
48
|
+
if (!isPy(fp)) continue;
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
// cursor.execute(f"SELECT ... {var}") or .execute("SELECT" + var) or .execute("SELECT %s" % var)
|
|
52
|
+
if (/\.(execute|executemany)\s*\(\s*f['"`]/.test(lines[i]) ||
|
|
53
|
+
/\.(execute|executemany)\s*\(\s*['"`].*\+/.test(lines[i]) ||
|
|
54
|
+
/\.(execute|executemany)\s*\(\s*['"`].*%s['"`]\s*%/.test(lines[i])) {
|
|
55
|
+
findings.push({
|
|
56
|
+
ruleId: 'BUG-PY-ADV-002', category: 'bugs', severity: 'high',
|
|
57
|
+
title: 'SQL injection — query built with string formatting instead of parameterized query',
|
|
58
|
+
description: 'Use cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) with parameter tuple instead of f-strings or % formatting.',
|
|
59
|
+
file: fp, line: i + 1, fix: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return findings;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'BUG-PY-ADV-003',
|
|
69
|
+
category: 'bugs',
|
|
70
|
+
severity: 'high',
|
|
71
|
+
confidence: 'likely',
|
|
72
|
+
title: 'except Exception catches too broadly',
|
|
73
|
+
check({ files }) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
for (const [fp, content] of files) {
|
|
76
|
+
if (!isPy(fp)) continue;
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
if (/^\s*except\s+Exception\s*(?:as\s+\w+)?\s*:/.test(lines[i])) {
|
|
80
|
+
// Check if it just logs and continues (swallowing)
|
|
81
|
+
let catchBody = '';
|
|
82
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
83
|
+
if (/^\S/.test(lines[j]) && lines[j].trim()) break;
|
|
84
|
+
catchBody += lines[j] + '\n';
|
|
85
|
+
}
|
|
86
|
+
if (/pass|print\(|logging\.\w+\(/.test(catchBody) && !/raise|return|sys\.exit/.test(catchBody)) {
|
|
87
|
+
findings.push({
|
|
88
|
+
ruleId: 'BUG-PY-ADV-003', category: 'bugs', severity: 'high',
|
|
89
|
+
title: 'except Exception swallows all errors — catch specific exceptions',
|
|
90
|
+
description: 'Catching Exception hides bugs like TypeError, KeyError, AttributeError. Catch specific exceptions you can handle.',
|
|
91
|
+
file: fp, line: i + 1, fix: null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return findings;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'BUG-PY-ADV-004',
|
|
102
|
+
category: 'bugs',
|
|
103
|
+
severity: 'medium',
|
|
104
|
+
confidence: 'definite',
|
|
105
|
+
title: 'os.system() used instead of subprocess',
|
|
106
|
+
check({ files }) {
|
|
107
|
+
const findings = [];
|
|
108
|
+
for (const [fp, content] of files) {
|
|
109
|
+
if (!isPy(fp)) continue;
|
|
110
|
+
const lines = content.split('\n');
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
if (/\bos\.system\s*\(/.test(lines[i])) {
|
|
113
|
+
findings.push({
|
|
114
|
+
ruleId: 'BUG-PY-ADV-004', category: 'bugs', severity: 'medium',
|
|
115
|
+
title: 'os.system() is insecure and has no error handling — use subprocess.run()',
|
|
116
|
+
description: 'os.system() runs through shell (injection risk), doesn\'t capture output, and returns exit code only. Use subprocess.run() with shell=False.',
|
|
117
|
+
file: fp, line: i + 1, fix: null,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'BUG-PY-ADV-005',
|
|
127
|
+
category: 'bugs',
|
|
128
|
+
severity: 'high',
|
|
129
|
+
confidence: 'likely',
|
|
130
|
+
title: 'async def without await inside',
|
|
131
|
+
check({ files }) {
|
|
132
|
+
const findings = [];
|
|
133
|
+
for (const [fp, content] of files) {
|
|
134
|
+
if (!isPy(fp)) continue;
|
|
135
|
+
const lines = content.split('\n');
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
if (/^\s*async\s+def\s+(\w+)/.test(lines[i])) {
|
|
138
|
+
const funcName = lines[i].match(/async\s+def\s+(\w+)/)[1];
|
|
139
|
+
let body = '';
|
|
140
|
+
let indent = null;
|
|
141
|
+
for (let j = i + 1; j < Math.min(i + 30, lines.length); j++) {
|
|
142
|
+
if (indent === null && lines[j].trim()) {
|
|
143
|
+
indent = lines[j].match(/^(\s*)/)[1].length;
|
|
144
|
+
}
|
|
145
|
+
if (indent !== null && lines[j].trim() && lines[j].match(/^(\s*)/)[1].length < indent) break;
|
|
146
|
+
body += lines[j] + '\n';
|
|
147
|
+
}
|
|
148
|
+
if (!body.includes('await') && !body.includes('async for') && !body.includes('async with') && !body.includes('yield')) {
|
|
149
|
+
findings.push({
|
|
150
|
+
ruleId: 'BUG-PY-ADV-005', category: 'bugs', severity: 'high',
|
|
151
|
+
title: `async function "${funcName}" never uses await — should be a regular function`,
|
|
152
|
+
description: 'Declaring a function async without using await adds overhead and confuses callers. Remove async or add proper await calls.',
|
|
153
|
+
file: fp, line: i + 1, fix: null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return findings;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'BUG-PY-ADV-006',
|
|
164
|
+
category: 'bugs',
|
|
165
|
+
severity: 'medium',
|
|
166
|
+
confidence: 'likely',
|
|
167
|
+
title: 'Global variable mutation in function',
|
|
168
|
+
check({ files }) {
|
|
169
|
+
const findings = [];
|
|
170
|
+
for (const [fp, content] of files) {
|
|
171
|
+
if (!isPy(fp)) continue;
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
if (/^\s+global\s+\w+/.test(lines[i])) {
|
|
175
|
+
const varName = lines[i].match(/global\s+(\w+)/)[1];
|
|
176
|
+
findings.push({
|
|
177
|
+
ruleId: 'BUG-PY-ADV-006', category: 'bugs', severity: 'medium',
|
|
178
|
+
title: `"global ${varName}" — global mutable state makes code unpredictable`,
|
|
179
|
+
description: 'Using global variables for state makes functions impure and hard to test. Pass state as parameters or use a class.',
|
|
180
|
+
file: fp, line: i + 1, fix: null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'BUG-PY-ADV-007',
|
|
190
|
+
category: 'bugs',
|
|
191
|
+
severity: 'high',
|
|
192
|
+
confidence: 'likely',
|
|
193
|
+
title: 'File opened without context manager',
|
|
194
|
+
check({ files }) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
for (const [fp, content] of files) {
|
|
197
|
+
if (!isPy(fp)) continue;
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
200
|
+
// f = open(...) without 'with'
|
|
201
|
+
if (/^\s*\w+\s*=\s*open\s*\(/.test(lines[i]) && !/^\s*with\b/.test(lines[i])) {
|
|
202
|
+
findings.push({
|
|
203
|
+
ruleId: 'BUG-PY-ADV-007', category: 'bugs', severity: 'high',
|
|
204
|
+
title: 'File opened without "with" context manager — may not be closed on error',
|
|
205
|
+
description: 'Always use `with open(...) as f:` to ensure the file is closed even if an exception occurs.',
|
|
206
|
+
file: fp, line: i + 1, fix: null,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'BUG-PY-ADV-008',
|
|
216
|
+
category: 'bugs',
|
|
217
|
+
severity: 'medium',
|
|
218
|
+
confidence: 'likely',
|
|
219
|
+
title: 'Hardcoded secret in Python source',
|
|
220
|
+
check({ files }) {
|
|
221
|
+
const findings = [];
|
|
222
|
+
for (const [fp, content] of files) {
|
|
223
|
+
if (!isPy(fp)) continue;
|
|
224
|
+
if (/test_|_test\.py|conftest|fixture/i.test(fp)) continue;
|
|
225
|
+
const lines = content.split('\n');
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
// SECRET_KEY = "..." or API_KEY = '...' or password = "..."
|
|
228
|
+
if (/^\s*(?:SECRET_KEY|API_KEY|DATABASE_URL|PASSWORD|PRIVATE_KEY|JWT_SECRET|ENCRYPTION_KEY)\s*=\s*['"`][^'"`]{8,}['"`]/.test(lines[i])) {
|
|
229
|
+
if (!/os\.environ|os\.getenv|environ\.get|config\[|settings\./.test(lines[i])) {
|
|
230
|
+
findings.push({
|
|
231
|
+
ruleId: 'BUG-PY-ADV-008', category: 'bugs', severity: 'medium',
|
|
232
|
+
title: 'Hardcoded secret in Python source — use environment variables',
|
|
233
|
+
description: 'Secrets should come from environment variables (os.environ) or a secrets manager, not hardcoded in source code.',
|
|
234
|
+
file: fp, line: i + 1, fix: null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return findings;
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
export default rules;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Auto-fixable Python bug rules
|
|
2
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
3
|
+
|
|
4
|
+
const rules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'BUG-PYFIX-001',
|
|
7
|
+
category: 'bugs',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
confidence: 'likely',
|
|
10
|
+
title: 'bare except → except Exception — auto-fixable',
|
|
11
|
+
check({ files }) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const [fp, content] of files) {
|
|
14
|
+
if (!isPy(fp)) continue;
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (/^(\s*)except\s*:\s*$/.test(lines[i])) {
|
|
18
|
+
const indent = lines[i].match(/^(\s*)/)[1];
|
|
19
|
+
findings.push({
|
|
20
|
+
ruleId: 'BUG-PYFIX-001', category: 'bugs', severity: 'high',
|
|
21
|
+
title: 'bare except: → except Exception: — auto-fixable',
|
|
22
|
+
file: fp, line: i + 1,
|
|
23
|
+
fix: { type: 'replace', old: lines[i], new: `${indent}except Exception:` },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'BUG-PYFIX-002',
|
|
33
|
+
category: 'bugs',
|
|
34
|
+
severity: 'high',
|
|
35
|
+
confidence: 'likely',
|
|
36
|
+
title: '== None → is None — auto-fixable',
|
|
37
|
+
check({ files }) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const [fp, content] of files) {
|
|
40
|
+
if (!isPy(fp)) continue;
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
if (/==\s*None/.test(lines[i])) {
|
|
44
|
+
findings.push({
|
|
45
|
+
ruleId: 'BUG-PYFIX-002', category: 'bugs', severity: 'high',
|
|
46
|
+
title: '== None → is None — auto-fixable',
|
|
47
|
+
file: fp, line: i + 1,
|
|
48
|
+
fix: { type: 'replace', old: '== None', new: 'is None' },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (/!=\s*None/.test(lines[i])) {
|
|
52
|
+
findings.push({
|
|
53
|
+
ruleId: 'BUG-PYFIX-002', category: 'bugs', severity: 'high',
|
|
54
|
+
title: '!= None → is not None — auto-fixable',
|
|
55
|
+
file: fp, line: i + 1,
|
|
56
|
+
fix: { type: 'replace', old: '!= None', new: 'is not None' },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return findings;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'BUG-PYFIX-003',
|
|
66
|
+
category: 'bugs',
|
|
67
|
+
severity: 'high',
|
|
68
|
+
confidence: 'likely',
|
|
69
|
+
title: 'except pass → except with logging — auto-fixable',
|
|
70
|
+
check({ files }) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
for (const [fp, content] of files) {
|
|
73
|
+
if (!isPy(fp)) continue;
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
if (/^\s*except\s+\w+/.test(lines[i]) && i + 1 < lines.length && /^\s+pass\s*$/.test(lines[i + 1])) {
|
|
77
|
+
const excLine = lines[i];
|
|
78
|
+
const passLine = lines[i + 1];
|
|
79
|
+
const indent = passLine.match(/^(\s*)/)[1];
|
|
80
|
+
// Add 'as e' if not present and replace pass with logging
|
|
81
|
+
if (!excLine.includes(' as ')) {
|
|
82
|
+
const newExc = excLine.replace(/:\s*$/, ' as e:');
|
|
83
|
+
findings.push({
|
|
84
|
+
ruleId: 'BUG-PYFIX-003', category: 'bugs', severity: 'high',
|
|
85
|
+
title: 'except/pass → except with logging — auto-fixable',
|
|
86
|
+
file: fp, line: i + 1,
|
|
87
|
+
fix: { type: 'replace', old: excLine + '\n' + passLine, new: newExc + '\n' + indent + 'logging.exception(e)' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return findings;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
export default rules;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Bug detection: Python-specific bugs
|
|
2
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
3
|
+
|
|
4
|
+
const rules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'BUG-PY-001',
|
|
7
|
+
category: 'bugs',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
confidence: 'likely',
|
|
10
|
+
title: 'Mutable default argument — list/dict shared across all calls',
|
|
11
|
+
check({ files }) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const [fp, content] of files) {
|
|
14
|
+
if (!isPy(fp)) continue;
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (/def\s+\w+\s*\(.*=\s*\[\s*\]/.test(lines[i]) || /def\s+\w+\s*\(.*=\s*\{\s*\}/.test(lines[i])) {
|
|
18
|
+
findings.push({
|
|
19
|
+
ruleId: 'BUG-PY-001', category: 'bugs', severity: 'high',
|
|
20
|
+
title: 'Mutable default argument — shared across all calls',
|
|
21
|
+
description: 'def f(items=[]): — the list is created once and shared between all calls. Use None as default and create inside: if items is None: items = []',
|
|
22
|
+
file: fp, line: i + 1, fix: null,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'BUG-PY-002',
|
|
32
|
+
category: 'bugs',
|
|
33
|
+
severity: 'high',
|
|
34
|
+
confidence: 'likely',
|
|
35
|
+
title: 'Bare except: catches SystemExit and KeyboardInterrupt',
|
|
36
|
+
check({ files }) {
|
|
37
|
+
const findings = [];
|
|
38
|
+
for (const [fp, content] of files) {
|
|
39
|
+
if (!isPy(fp)) continue;
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
if (/^\s*except\s*:/.test(lines[i])) {
|
|
43
|
+
findings.push({
|
|
44
|
+
ruleId: 'BUG-PY-002', category: 'bugs', severity: 'high',
|
|
45
|
+
title: 'Bare except: catches SystemExit and KeyboardInterrupt',
|
|
46
|
+
description: 'except: catches everything including sys.exit() and Ctrl+C. Use except Exception: to only catch actual errors.',
|
|
47
|
+
file: fp, line: i + 1, fix: null,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return findings;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'BUG-PY-003',
|
|
57
|
+
category: 'bugs',
|
|
58
|
+
severity: 'high',
|
|
59
|
+
confidence: 'likely',
|
|
60
|
+
title: 'Using "is" to compare values instead of ==',
|
|
61
|
+
check({ files }) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
for (const [fp, content] of files) {
|
|
64
|
+
if (!isPy(fp)) continue;
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i];
|
|
68
|
+
// "is" with string/number literals (not None, True, False)
|
|
69
|
+
if (/\bis\s+['"\d]/.test(line) || /\bis\s+[-+]\d/.test(line)) {
|
|
70
|
+
if (!line.includes('is None') && !line.includes('is True') && !line.includes('is False') && !line.includes('is not None')) {
|
|
71
|
+
findings.push({
|
|
72
|
+
ruleId: 'BUG-PY-003', category: 'bugs', severity: 'high',
|
|
73
|
+
title: '"is" compares identity, not value — use == for values',
|
|
74
|
+
description: '"x is 5" checks if they are the same object in memory, not if they have the same value. CPython caches small ints (-5 to 256) but this is an implementation detail.',
|
|
75
|
+
file: fp, line: i + 1, fix: null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'BUG-PY-004',
|
|
86
|
+
category: 'bugs',
|
|
87
|
+
severity: 'high',
|
|
88
|
+
confidence: 'likely',
|
|
89
|
+
title: 'Modifying list during iteration',
|
|
90
|
+
check({ files }) {
|
|
91
|
+
const findings = [];
|
|
92
|
+
for (const [fp, content] of files) {
|
|
93
|
+
if (!isPy(fp)) continue;
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const forMatch = lines[i].match(/for\s+\w+\s+in\s+(\w+)/);
|
|
97
|
+
if (forMatch) {
|
|
98
|
+
const listName = forMatch[1];
|
|
99
|
+
const body = lines.slice(i + 1, Math.min(lines.length, i + 15)).join('\n');
|
|
100
|
+
const modPattern = new RegExp(listName + '\\s*\\.\\s*(?:append|remove|pop|insert|extend|clear)\\s*\\(');
|
|
101
|
+
if (modPattern.test(body)) {
|
|
102
|
+
findings.push({
|
|
103
|
+
ruleId: 'BUG-PY-004', category: 'bugs', severity: 'high',
|
|
104
|
+
title: `Modifying "${listName}" while iterating over it`,
|
|
105
|
+
description: 'Modifying a list during iteration causes skipped elements or RuntimeError. Iterate over a copy: for item in list(items):',
|
|
106
|
+
file: fp, line: i + 1, fix: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return findings;
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'BUG-PY-005',
|
|
117
|
+
category: 'bugs',
|
|
118
|
+
severity: 'high',
|
|
119
|
+
confidence: 'likely',
|
|
120
|
+
title: 'Late-binding closure in loop — variable captured by reference',
|
|
121
|
+
check({ files }) {
|
|
122
|
+
const findings = [];
|
|
123
|
+
for (const [fp, content] of files) {
|
|
124
|
+
if (!isPy(fp)) continue;
|
|
125
|
+
const lines = content.split('\n');
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
if (/for\s+\w+\s+in\s+/.test(lines[i])) {
|
|
128
|
+
const body = lines.slice(i + 1, Math.min(lines.length, i + 10)).join('\n');
|
|
129
|
+
if (/lambda\b/.test(body) && !body.includes('=i') && !body.includes('=x')) {
|
|
130
|
+
findings.push({
|
|
131
|
+
ruleId: 'BUG-PY-005', category: 'bugs', severity: 'high',
|
|
132
|
+
title: 'Lambda in loop captures variable by reference — all share final value',
|
|
133
|
+
description: 'Closures in Python capture variables by reference. All lambdas will use the loop variable\'s final value. Fix: lambda x=x: ...',
|
|
134
|
+
file: fp, line: i + 1, fix: null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return findings;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'BUG-PY-006',
|
|
145
|
+
category: 'bugs',
|
|
146
|
+
severity: 'high',
|
|
147
|
+
confidence: 'likely',
|
|
148
|
+
title: 'except Exception as e: pass — error silently swallowed',
|
|
149
|
+
check({ files }) {
|
|
150
|
+
const findings = [];
|
|
151
|
+
for (const [fp, content] of files) {
|
|
152
|
+
if (!isPy(fp)) continue;
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
155
|
+
if (/^\s*except\b/.test(lines[i]) && i + 1 < lines.length && /^\s+pass\s*$/.test(lines[i + 1])) {
|
|
156
|
+
findings.push({
|
|
157
|
+
ruleId: 'BUG-PY-006', category: 'bugs', severity: 'high',
|
|
158
|
+
title: 'except/pass — errors silently swallowed',
|
|
159
|
+
description: 'Catching exceptions and doing nothing (pass) hides bugs. At minimum, log the error.',
|
|
160
|
+
file: fp, line: i + 1, fix: null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return findings;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'BUG-PY-007',
|
|
170
|
+
category: 'bugs',
|
|
171
|
+
severity: 'high',
|
|
172
|
+
confidence: 'likely',
|
|
173
|
+
title: 'f-string without f prefix — curly braces treated as literal',
|
|
174
|
+
check({ files }) {
|
|
175
|
+
const findings = [];
|
|
176
|
+
for (const [fp, content] of files) {
|
|
177
|
+
if (!isPy(fp)) continue;
|
|
178
|
+
const lines = content.split('\n');
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
const line = lines[i];
|
|
181
|
+
// Detect string with {variable} that's not an f-string
|
|
182
|
+
if (/[^f]["']\s*.*\{[a-zA-Z_]\w*\}/.test(line) && !line.includes('.format') && !line.includes('{{')) {
|
|
183
|
+
// Exclude dict literals and class definitions
|
|
184
|
+
if (!/(?:dict|class|import|def\s|return\s*\{|\w+\s*=\s*\{)/.test(line)) {
|
|
185
|
+
findings.push({
|
|
186
|
+
ruleId: 'BUG-PY-007', category: 'bugs', severity: 'high',
|
|
187
|
+
title: 'String with {variable} but no f-prefix — variables not interpolated',
|
|
188
|
+
description: 'This looks like it should be an f-string. Without the f prefix, {variable} is treated as a literal string.',
|
|
189
|
+
file: fp, line: i + 1, fix: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return findings;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'BUG-PY-008',
|
|
200
|
+
category: 'bugs',
|
|
201
|
+
severity: 'high',
|
|
202
|
+
confidence: 'likely',
|
|
203
|
+
title: 'Missing self parameter in method',
|
|
204
|
+
check({ files }) {
|
|
205
|
+
const findings = [];
|
|
206
|
+
for (const [fp, content] of files) {
|
|
207
|
+
if (!isPy(fp)) continue;
|
|
208
|
+
const lines = content.split('\n');
|
|
209
|
+
let inClass = false;
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
if (/^class\s+\w+/.test(lines[i])) inClass = true;
|
|
212
|
+
if (inClass && /^\s{4}def\s+\w+\s*\(\s*\)/.test(lines[i])) {
|
|
213
|
+
const funcName = lines[i].match(/def\s+(\w+)/)[1];
|
|
214
|
+
if (funcName !== '__init__' && !lines[i - 1]?.includes('@staticmethod') && !lines[i - 1]?.includes('@classmethod')) {
|
|
215
|
+
findings.push({
|
|
216
|
+
ruleId: 'BUG-PY-008', category: 'bugs', severity: 'high',
|
|
217
|
+
title: `Method "${funcName}" missing self parameter`,
|
|
218
|
+
description: 'Instance methods must take self as first parameter. Without it, calling the method raises TypeError.',
|
|
219
|
+
file: fp, line: i + 1, fix: null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (/^[^\s]/.test(lines[i]) && !/^class/.test(lines[i]) && inClass) inClass = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return findings;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'BUG-PY-009',
|
|
231
|
+
category: 'bugs',
|
|
232
|
+
severity: 'high',
|
|
233
|
+
confidence: 'likely',
|
|
234
|
+
title: 'Using == to compare with None — use "is None"',
|
|
235
|
+
check({ files }) {
|
|
236
|
+
const findings = [];
|
|
237
|
+
for (const [fp, content] of files) {
|
|
238
|
+
if (!isPy(fp)) continue;
|
|
239
|
+
const lines = content.split('\n');
|
|
240
|
+
for (let i = 0; i < lines.length; i++) {
|
|
241
|
+
if (/==\s*None\b/.test(lines[i]) || /!=\s*None\b/.test(lines[i])) {
|
|
242
|
+
findings.push({
|
|
243
|
+
ruleId: 'BUG-PY-009', category: 'bugs', severity: 'high',
|
|
244
|
+
title: 'Use "is None" instead of "== None"',
|
|
245
|
+
description: '== can be overridden by __eq__. PEP 8 recommends "is None" for singleton comparison.',
|
|
246
|
+
file: fp, line: i + 1, fix: null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return findings;
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'BUG-PY-010',
|
|
256
|
+
category: 'bugs',
|
|
257
|
+
severity: 'medium',
|
|
258
|
+
confidence: 'likely',
|
|
259
|
+
title: 'String concatenation in loop — use join() or list',
|
|
260
|
+
check({ files }) {
|
|
261
|
+
const findings = [];
|
|
262
|
+
for (const [fp, content] of files) {
|
|
263
|
+
if (!isPy(fp)) continue;
|
|
264
|
+
const lines = content.split('\n');
|
|
265
|
+
let inLoop = false;
|
|
266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
267
|
+
if (/^\s*(for|while)\s+/.test(lines[i])) inLoop = true;
|
|
268
|
+
if (inLoop && /\w+\s*\+=\s*['"]/.test(lines[i])) {
|
|
269
|
+
findings.push({
|
|
270
|
+
ruleId: 'BUG-PY-010', category: 'bugs', severity: 'medium',
|
|
271
|
+
title: 'String concatenation in loop — O(n^2), use list + join()',
|
|
272
|
+
description: 'Strings are immutable in Python. += creates a new string each iteration. Append to a list and use "".join() at the end.',
|
|
273
|
+
file: fp, line: i + 1, fix: null,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (/^[^\s]/.test(lines[i]) && inLoop && !/^\s*(for|while|if|else|elif|try|except|with)/.test(lines[i])) inLoop = false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return findings;
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
export default rules;
|