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,214 @@
1
+ // Bug detection: Memory leak patterns
2
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
3
+ function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
4
+
5
+ const rules = [
6
+ {
7
+ id: 'BUG-MEM-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'likely',
11
+ title: 'setInterval without clearInterval',
12
+ check({ files }) {
13
+ const findings = [];
14
+ for (const [fp, content] of files) {
15
+ if (!isJS(fp)) continue;
16
+ const lines = content.split('\n');
17
+ for (let i = 0; i < lines.length; i++) {
18
+ if (/setInterval\s*\(/.test(lines[i])) {
19
+ // Check if interval is stored and cleared
20
+ const hasStore = /(?:const|let|var)\s+\w+\s*=\s*setInterval|intervalId|intervalRef|timer/.test(lines[i]);
21
+ const hasClear = /clearInterval/.test(content);
22
+ if (!hasClear) {
23
+ findings.push({
24
+ ruleId: 'BUG-MEM-001', category: 'bugs', severity: 'high',
25
+ title: 'setInterval without clearInterval — keeps running forever',
26
+ description: 'Intervals run indefinitely. In components, they leak on unmount. In servers, they prevent garbage collection. Always store the ID and clear it.',
27
+ file: fp, line: i + 1, fix: null,
28
+ });
29
+ } else if (!hasStore) {
30
+ findings.push({
31
+ ruleId: 'BUG-MEM-001', category: 'bugs', severity: 'high',
32
+ title: 'setInterval return value not stored — can never be cleared',
33
+ description: 'Store the interval ID to clear it later: const id = setInterval(...); clearInterval(id);',
34
+ file: fp, line: i + 1, fix: null,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return findings;
41
+ },
42
+ },
43
+ {
44
+ id: 'BUG-MEM-002',
45
+ category: 'bugs',
46
+ severity: 'high',
47
+ confidence: 'likely',
48
+ title: 'addEventListener without removeEventListener',
49
+ check({ files }) {
50
+ const findings = [];
51
+ for (const [fp, content] of files) {
52
+ if (!isJS(fp)) continue;
53
+ // Count add vs remove
54
+ const adds = (content.match(/addEventListener\s*\(/g) || []).length;
55
+ const removes = (content.match(/removeEventListener\s*\(/g) || []).length;
56
+ if (adds > 0 && removes === 0) {
57
+ // Check if it's in a component (has React imports or hooks)
58
+ const isComponent = /use(State|Effect|Ref|Callback)|React|Component/.test(content);
59
+ if (isComponent) {
60
+ const lines = content.split('\n');
61
+ for (let i = 0; i < lines.length; i++) {
62
+ if (/addEventListener\s*\(/.test(lines[i])) {
63
+ findings.push({
64
+ ruleId: 'BUG-MEM-002', category: 'bugs', severity: 'high',
65
+ title: 'addEventListener in component without removeEventListener — memory leak',
66
+ description: 'Event listeners must be cleaned up on unmount. Add removeEventListener in the useEffect cleanup function.',
67
+ file: fp, line: i + 1, fix: null,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return findings;
75
+ },
76
+ },
77
+ {
78
+ id: 'BUG-MEM-003',
79
+ category: 'bugs',
80
+ severity: 'medium',
81
+ confidence: 'likely',
82
+ title: 'Growing array/map in module scope — unbounded memory growth',
83
+ check({ files }) {
84
+ const findings = [];
85
+ for (const [fp, content] of files) {
86
+ if (!isJS(fp)) continue;
87
+ const lines = content.split('\n');
88
+ // Find module-level arrays/maps/sets
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const match = lines[i].match(/^(?:const|let)\s+(\w+)\s*=\s*(?:new\s+(?:Map|Set|WeakMap)\s*\(\)|\[\s*\]|\{\s*\})/);
91
+ if (!match) continue;
92
+ const name = match[1];
93
+ // Check if items are added but never removed/cleared
94
+ const addRe = new RegExp(`${name}\\s*\\.\\s*(push|add|set|unshift)\\s*\\(`);
95
+ const removeRe = new RegExp(`${name}\\s*\\.\\s*(delete|remove|splice|pop|shift|clear|slice)\\s*\\(`);
96
+ const resetRe = new RegExp(`^\\s*${name}\\s*=\\s*(?:new|\\[\\s*\\]|\\{\\s*\\})`, 'm');
97
+ // Check rest of file (after declaration) for add/remove
98
+ const rest = lines.slice(i + 1).join('\n');
99
+ if (addRe.test(rest) && !removeRe.test(rest) && !resetRe.test(rest)) {
100
+ findings.push({
101
+ ruleId: 'BUG-MEM-003', category: 'bugs', severity: 'medium',
102
+ title: `Module-level "${name}" grows without bounds — items added but never removed`,
103
+ description: 'Collections in module scope persist for the process lifetime. Without cleanup, they grow indefinitely and leak memory.',
104
+ file: fp, line: i + 1, fix: null,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ return findings;
110
+ },
111
+ },
112
+ {
113
+ id: 'BUG-MEM-004',
114
+ category: 'bugs',
115
+ severity: 'high',
116
+ confidence: 'likely',
117
+ title: 'Closure captures large object unnecessarily',
118
+ check({ files }) {
119
+ const findings = [];
120
+ for (const [fp, content] of files) {
121
+ if (!isJS(fp)) continue;
122
+ const lines = content.split('\n');
123
+ for (let i = 0; i < lines.length; i++) {
124
+ // Large data read, then closure created that captures it
125
+ if (/(?:const|let)\s+(\w+)\s*=\s*(?:await\s+)?(?:readFile|fs\.read|JSON\.parse|Buffer\.from|response\.json|\.text\(\)|fetch)/.test(lines[i])) {
126
+ const varName = lines[i].match(/(?:const|let)\s+(\w+)/)[1];
127
+ // Check if a long-lived closure references this (setTimeout, setInterval, event handler)
128
+ for (let j = i + 1; j < Math.min(i + 20, lines.length); j++) {
129
+ if (/(?:setTimeout|setInterval|addEventListener|\.on\(|new\s+Promise|=>\s*\{)/.test(lines[j])) {
130
+ const closureBlock = lines.slice(j, Math.min(j + 15, lines.length)).join('\n');
131
+ if (new RegExp(`\\b${varName}\\b`).test(closureBlock)) {
132
+ findings.push({
133
+ ruleId: 'BUG-MEM-004', category: 'bugs', severity: 'high',
134
+ title: `Large data "${varName}" captured by long-lived closure — may leak memory`,
135
+ description: 'Closures in timers and event handlers keep captured variables alive. If the variable holds large data (file contents, API responses), it can\'t be garbage collected.',
136
+ file: fp, line: j + 1, fix: null,
137
+ });
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return findings;
146
+ },
147
+ },
148
+ {
149
+ id: 'BUG-MEM-005',
150
+ category: 'bugs',
151
+ severity: 'medium',
152
+ confidence: 'likely',
153
+ title: 'String concatenation in loop — creates many intermediate strings',
154
+ check({ files }) {
155
+ const findings = [];
156
+ for (const [fp, content] of files) {
157
+ if (!isJS(fp)) continue;
158
+ const lines = content.split('\n');
159
+ let inLoop = false;
160
+ for (let i = 0; i < lines.length; i++) {
161
+ if (/^\s*(?:for|while)\s*\(/.test(lines[i])) inLoop = true;
162
+ if (inLoop && /\w+\s*\+=\s*['"`]/.test(lines[i])) {
163
+ findings.push({
164
+ ruleId: 'BUG-MEM-005', category: 'bugs', severity: 'medium',
165
+ title: 'String concatenation in loop — O(n²) memory and time',
166
+ description: 'Each += creates a new string. For large loops, use an array and .join("") at the end.',
167
+ file: fp, line: i + 1, fix: null,
168
+ });
169
+ inLoop = false;
170
+ }
171
+ if (/^\s*\}/.test(lines[i])) inLoop = false;
172
+ }
173
+ }
174
+ return findings;
175
+ },
176
+ },
177
+ {
178
+ id: 'BUG-MEM-006',
179
+ category: 'bugs',
180
+ severity: 'medium',
181
+ confidence: 'likely',
182
+ title: 'setTimeout in recursive function without base case check',
183
+ check({ files }) {
184
+ const findings = [];
185
+ for (const [fp, content] of files) {
186
+ if (!isJS(fp)) continue;
187
+ const lines = content.split('\n');
188
+ for (let i = 0; i < lines.length; i++) {
189
+ const funcMatch = lines[i].match(/(?:function\s+(\w+)|(?:const|let)\s+(\w+)\s*=)/);
190
+ if (!funcMatch) continue;
191
+ const funcName = funcMatch[1] || funcMatch[2];
192
+ // Check if function calls setTimeout with itself
193
+ let block = '';
194
+ for (let j = i; j < Math.min(i + 25, lines.length); j++) {
195
+ block += lines[j] + '\n';
196
+ }
197
+ if (new RegExp(`setTimeout\\s*\\(\\s*${funcName}\\b`).test(block) || new RegExp(`setTimeout\\s*\\(\\s*\\(\\)\\s*=>\\s*${funcName}\\s*\\(`).test(block)) {
198
+ if (!/if\s*\(|return\s|clearTimeout|cancel|stop/.test(block)) {
199
+ findings.push({
200
+ ruleId: 'BUG-MEM-006', category: 'bugs', severity: 'medium',
201
+ title: `Recursive setTimeout for "${funcName}" without clear stop condition`,
202
+ description: 'setTimeout calling the same function without a stop condition runs forever. Add a base case or cancellation mechanism.',
203
+ file: fp, line: i + 1, fix: null,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return findings;
210
+ },
211
+ },
212
+ ];
213
+
214
+ export default rules;
@@ -0,0 +1,361 @@
1
+ // Bug detection: Node.js backend patterns Claude commonly generates wrong
2
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
3
+ function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
4
+
5
+ const rules = [
6
+ {
7
+ id: 'BUG-NODE-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'definite',
11
+ title: 'Express route handler without error handling middleware',
12
+ check({ files }) {
13
+ const findings = [];
14
+ for (const [fp, content] of files) {
15
+ if (!isJS(fp)) continue;
16
+ const lines = content.split('\n');
17
+ for (let i = 0; i < lines.length; i++) {
18
+ // async route handler without try/catch
19
+ if (/\.(get|post|put|patch|delete)\s*\(\s*['"`\/]/.test(lines[i])) {
20
+ let block = '';
21
+ for (let j = i; j < Math.min(i + 20, lines.length); j++) {
22
+ block += lines[j] + '\n';
23
+ }
24
+ if (/async\s*\(/.test(block) && !/try\s*\{/.test(block) && !/asyncHandler|catchAsync|wrapper|middleware/.test(block)) {
25
+ findings.push({
26
+ ruleId: 'BUG-NODE-001', category: 'bugs', severity: 'high',
27
+ title: 'Async Express route without try/catch — unhandled rejections crash the server',
28
+ description: 'Express does not catch async errors automatically. Wrap in try/catch or use an async error wrapper.',
29
+ file: fp, line: i + 1, fix: null,
30
+ });
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return findings;
36
+ },
37
+ },
38
+ {
39
+ id: 'BUG-NODE-002',
40
+ category: 'bugs',
41
+ severity: 'high',
42
+ confidence: 'definite',
43
+ title: 'Response sent after headers already sent',
44
+ check({ files }) {
45
+ const findings = [];
46
+ for (const [fp, content] of files) {
47
+ if (!isJS(fp)) continue;
48
+ const lines = content.split('\n');
49
+ for (let i = 0; i < lines.length; i++) {
50
+ // res.send/json/end followed by more res.send/json/end without return
51
+ if (/res\.(send|json|end|redirect|render|status\(\d+\)\.(send|json))\s*\(/.test(lines[i])) {
52
+ if (!/return\s+res\./.test(lines[i]) && !/^\s*return\b/.test(lines[i])) {
53
+ // Check next few lines for another response
54
+ for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
55
+ if (/res\.(send|json|end|redirect|render|status)\s*\(/.test(lines[j])) {
56
+ findings.push({
57
+ ruleId: 'BUG-NODE-002', category: 'bugs', severity: 'high',
58
+ title: 'Multiple responses sent — "headers already sent" crash',
59
+ description: 'Sending a response without returning allows subsequent code to send another response. Use `return res.json(...)` to prevent this.',
60
+ file: fp, line: i + 1, fix: null,
61
+ });
62
+ break;
63
+ }
64
+ if (/return\b|\}/.test(lines[j])) break;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return findings;
71
+ },
72
+ },
73
+ {
74
+ id: 'BUG-NODE-003',
75
+ category: 'bugs',
76
+ severity: 'high',
77
+ confidence: 'likely',
78
+ title: 'Callback called multiple times',
79
+ check({ files }) {
80
+ const findings = [];
81
+ for (const [fp, content] of files) {
82
+ if (!isJS(fp)) continue;
83
+ const lines = content.split('\n');
84
+ for (let i = 0; i < lines.length; i++) {
85
+ // callback/cb/next called without return before it
86
+ if (/\b(callback|cb|next|done)\s*\(/.test(lines[i]) && !/return\s+(callback|cb|next|done)/.test(lines[i]) && !/^\s*return\b/.test(lines[i])) {
87
+ // Check if same callback is called again within next 10 lines
88
+ const cbName = lines[i].match(/\b(callback|cb|next|done)\s*\(/)?.[1];
89
+ if (!cbName) continue;
90
+ for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
91
+ if (new RegExp(`\\b${cbName}\\s*\\(`).test(lines[j])) {
92
+ findings.push({
93
+ ruleId: 'BUG-NODE-003', category: 'bugs', severity: 'high',
94
+ title: `Callback "${cbName}" may be called multiple times`,
95
+ description: 'Calling a callback without returning first allows it to be called again. Use `return callback(...)` to prevent double-calls.',
96
+ file: fp, line: i + 1, fix: null,
97
+ });
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return findings;
105
+ },
106
+ },
107
+ {
108
+ id: 'BUG-NODE-004',
109
+ category: 'bugs',
110
+ severity: 'high',
111
+ confidence: 'definite',
112
+ title: 'Synchronous file I/O in request handler',
113
+ check({ files }) {
114
+ const findings = [];
115
+ for (const [fp, content] of files) {
116
+ if (!isJS(fp)) continue;
117
+ const lines = content.split('\n');
118
+ const isServerFile = /express|fastify|koa|http\.createServer|app\.(get|post|use)|router\.(get|post)/.test(content);
119
+ if (!isServerFile) continue;
120
+ for (let i = 0; i < lines.length; i++) {
121
+ if (/\bfs\.(readFileSync|writeFileSync|appendFileSync|mkdirSync|readdirSync|statSync|existsSync|unlinkSync)\s*\(/.test(lines[i])) {
122
+ findings.push({
123
+ ruleId: 'BUG-NODE-004', category: 'bugs', severity: 'high',
124
+ title: 'Synchronous fs call in server code blocks the event loop',
125
+ description: 'Sync I/O in server code blocks all concurrent requests. Use async alternatives (fs.promises.readFile, etc.).',
126
+ file: fp, line: i + 1, fix: null,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ return findings;
132
+ },
133
+ },
134
+ {
135
+ id: 'BUG-NODE-005',
136
+ category: 'bugs',
137
+ severity: 'high',
138
+ confidence: 'likely',
139
+ title: 'Unhandled stream error event',
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
+ // createReadStream/createWriteStream without .on('error')
147
+ if (/\b(createReadStream|createWriteStream)\s*\(/.test(lines[i])) {
148
+ let block = '';
149
+ for (let j = i; j < Math.min(i + 10, lines.length); j++) {
150
+ block += lines[j] + '\n';
151
+ }
152
+ if (!block.includes(".on('error'") && !block.includes('.on("error"') && !block.includes('pipeline') && !block.includes('stream.finished')) {
153
+ findings.push({
154
+ ruleId: 'BUG-NODE-005', category: 'bugs', severity: 'high',
155
+ title: 'Stream created without error handler — crashes on file not found',
156
+ description: 'Node.js streams emit "error" events that crash the process if unhandled. Always add .on("error", handler) or use stream.pipeline().',
157
+ file: fp, line: i + 1, fix: null,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return findings;
164
+ },
165
+ },
166
+ {
167
+ id: 'BUG-NODE-006',
168
+ category: 'bugs',
169
+ severity: 'medium',
170
+ confidence: 'likely',
171
+ title: 'process.env used without validation',
172
+ check({ files }) {
173
+ const findings = [];
174
+ for (const [fp, content] of files) {
175
+ if (!isJS(fp)) continue;
176
+ const lines = content.split('\n');
177
+ const envVars = new Set();
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const matches = lines[i].matchAll(/process\.env\.(\w+)/g);
180
+ for (const m of matches) {
181
+ envVars.add(m[1]);
182
+ }
183
+ }
184
+ // Check if any env vars are used directly without checking if defined
185
+ const hasEnvValidation = /process\.env\.\w+\s*\|\||process\.env\.\w+\s*\?\?|if\s*\(\s*!?\s*process\.env|dotenv|envalid|joi.*process\.env|z\.\w+.*process\.env/.test(content);
186
+ if (envVars.size > 3 && !hasEnvValidation) {
187
+ findings.push({
188
+ ruleId: 'BUG-NODE-006', category: 'bugs', severity: 'medium',
189
+ title: `${envVars.size} env vars used without validation — undefined at runtime`,
190
+ description: 'Environment variables are undefined by default. Validate required env vars at startup to fail fast instead of silently using "undefined".',
191
+ file: fp, line: 1, fix: null,
192
+ });
193
+ }
194
+ }
195
+ return findings;
196
+ },
197
+ },
198
+ {
199
+ id: 'BUG-NODE-007',
200
+ category: 'bugs',
201
+ severity: 'high',
202
+ confidence: 'likely',
203
+ title: 'Event emitter listener leak',
204
+ check({ files }) {
205
+ const findings = [];
206
+ for (const [fp, content] of files) {
207
+ if (!isJS(fp)) continue;
208
+ const lines = content.split('\n');
209
+ for (let i = 0; i < lines.length; i++) {
210
+ // .on() inside a function that could be called many times without .off() or removeListener
211
+ if (/\.(on|addListener)\s*\(\s*['"`]/.test(lines[i])) {
212
+ // Check if we're inside a route handler or recurring function
213
+ const context = lines.slice(Math.max(0, i - 10), i).join('\n');
214
+ if (/\.(get|post|put|delete)\s*\(|function\s+\w+|=>\s*\{/.test(context)) {
215
+ const afterContext = lines.slice(i, Math.min(i + 20, lines.length)).join('\n');
216
+ if (!/\.(off|removeListener|removeAllListeners)\s*\(/.test(afterContext) && !/once\s*\(/.test(lines[i])) {
217
+ findings.push({
218
+ ruleId: 'BUG-NODE-007', category: 'bugs', severity: 'high',
219
+ title: 'Event listener added in potentially recurring context without removal',
220
+ description: 'Adding listeners inside functions called multiple times causes memory leaks. Use .once() or remove listeners when done.',
221
+ file: fp, line: i + 1, fix: null,
222
+ });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ return findings;
229
+ },
230
+ },
231
+ {
232
+ id: 'BUG-NODE-008',
233
+ category: 'bugs',
234
+ severity: 'medium',
235
+ confidence: 'likely',
236
+ title: 'JSON.stringify on circular structure',
237
+ check({ files }) {
238
+ const findings = [];
239
+ for (const [fp, content] of files) {
240
+ if (!isJS(fp)) continue;
241
+ const lines = content.split('\n');
242
+ for (let i = 0; i < lines.length; i++) {
243
+ // JSON.stringify(req) or JSON.stringify(res) or JSON.stringify(socket) — common circular refs
244
+ if (/JSON\.stringify\s*\(\s*(req|res|socket|server|app|connection|ctx)\b/.test(lines[i])) {
245
+ findings.push({
246
+ ruleId: 'BUG-NODE-008', category: 'bugs', severity: 'medium',
247
+ title: 'JSON.stringify on likely circular object (req/res/socket) — throws TypeError',
248
+ description: 'Express req/res objects contain circular references. JSON.stringify will throw. Serialize specific properties instead.',
249
+ file: fp, line: i + 1, fix: null,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ return findings;
255
+ },
256
+ },
257
+ {
258
+ id: 'BUG-NODE-009',
259
+ category: 'bugs',
260
+ severity: 'high',
261
+ confidence: 'likely',
262
+ title: 'Missing await on database operation',
263
+ check({ files }) {
264
+ const findings = [];
265
+ for (const [fp, content] of files) {
266
+ if (!isJS(fp)) continue;
267
+ const lines = content.split('\n');
268
+ for (let i = 0; i < lines.length; i++) {
269
+ const line = lines[i];
270
+ // DB operations that return promises: .find(), .save(), .create(), .findOne(), .updateOne(), .deleteOne()
271
+ if (/\b(Model|model|db|collection|prisma|mongoose|knex|sequelize)\b/.test(content)) {
272
+ if (/\.\s*(find|findOne|findById|findMany|create|save|update|updateOne|updateMany|delete|deleteOne|deleteMany|insertOne|insertMany|aggregate|count|countDocuments)\s*\(/.test(line)) {
273
+ if (!/await\s/.test(line) && !/return\s/.test(line) && !/\.then\s*\(/.test(line) && !/^\s*const\s+\w+\s*=/.test(line)) {
274
+ findings.push({
275
+ ruleId: 'BUG-NODE-009', category: 'bugs', severity: 'high',
276
+ title: 'Database operation without await — data not actually saved/fetched',
277
+ description: 'Database calls return Promises. Without await, the operation fires but the result is discarded.',
278
+ file: fp, line: i + 1, fix: null,
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ return findings;
286
+ },
287
+ },
288
+ {
289
+ id: 'BUG-NODE-010',
290
+ category: 'bugs',
291
+ severity: 'medium',
292
+ confidence: 'likely',
293
+ title: 'Middleware next() called but execution continues',
294
+ check({ files }) {
295
+ const findings = [];
296
+ for (const [fp, content] of files) {
297
+ if (!isJS(fp)) continue;
298
+ const lines = content.split('\n');
299
+ for (let i = 0; i < lines.length; i++) {
300
+ // next() without return, followed by more code
301
+ if (/^\s*next\s*\(\s*\)\s*;?\s*$/.test(lines[i])) {
302
+ // Check if there's meaningful code after next() before the function ends
303
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
304
+ const nextLine = lines[j].trim();
305
+ if (nextLine === '}' || nextLine === '});' || nextLine === '') continue;
306
+ if (/^\s*(res\.|console\.|\/\/)/.test(lines[j])) {
307
+ findings.push({
308
+ ruleId: 'BUG-NODE-010', category: 'bugs', severity: 'medium',
309
+ title: 'Code after next() — middleware continues executing after passing control',
310
+ description: 'Calling next() does not stop the current middleware. Use `return next()` to prevent continued execution.',
311
+ file: fp, line: i + 1, fix: null,
312
+ });
313
+ break;
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return findings;
320
+ },
321
+ },
322
+
323
+ // BUG-NODE-011: Buffer.from(string) without encoding
324
+ {
325
+ id: 'BUG-NODE-011',
326
+ category: 'bugs',
327
+ severity: 'medium',
328
+ confidence: 'suggestion',
329
+ title: 'Buffer.from(variable) without encoding — may produce wrong result',
330
+ check({ files }) {
331
+ const findings = [];
332
+ for (const [fp, content] of files) {
333
+ if (!isJS(fp)) continue;
334
+ const lines = content.split('\n');
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const line = lines[i];
337
+ if (line.trim().startsWith('//')) continue;
338
+ // Buffer.from(variable) with only one argument that's a variable (not a string literal or array)
339
+ const match = line.match(/Buffer\.from\s*\(\s*(\w+)\s*\)/);
340
+ if (!match) continue;
341
+ const arg = match[1];
342
+ // Skip if it's a known safe pattern (array, buffer)
343
+ if (/^['"0-9\[]/.test(arg)) continue;
344
+ // Check if there's a hex/base64/binary context nearby suggesting encoding needed
345
+ const context = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join(' ');
346
+ if (/hex|base64|binary|latin1|ascii|utf16le/i.test(context)) {
347
+ findings.push({
348
+ ruleId: 'BUG-NODE-011', category: 'bugs', severity: 'medium',
349
+ title: `Buffer.from(${arg}) without encoding — context suggests encoding needed`,
350
+ description: `Buffer.from(string) defaults to UTF-8 encoding. If "${arg}" contains hex or base64 data, pass the encoding explicitly: Buffer.from(${arg}, 'hex') or Buffer.from(${arg}, 'base64').`,
351
+ file: fp, line: i + 1, fix: null,
352
+ });
353
+ }
354
+ }
355
+ }
356
+ return findings;
357
+ },
358
+ },
359
+ ];
360
+
361
+ export default rules;