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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. 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;