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,361 @@
1
+ // Bug detection: Universal patterns across all languages
2
+ function isSource(f) {
3
+ return ['.js','.jsx','.ts','.tsx','.mjs','.cjs','.py','.rb','.go','.java','.cs','.php','.rs','.swift','.dart','.sh'].some(e => f.endsWith(e));
4
+ }
5
+
6
+ const rules = [
7
+ {
8
+ id: 'BUG-GEN-001',
9
+ category: 'bugs',
10
+ severity: 'high',
11
+ confidence: 'likely',
12
+ title: 'TODO/FIXME/HACK/BUG in production code',
13
+ check({ files }) {
14
+ const findings = [];
15
+ for (const [fp, content] of files) {
16
+ if (!isSource(fp)) continue;
17
+ const lines = content.split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const match = lines[i].match(/\/\/\s*(TODO|FIXME|HACK|BUG|XXX|BROKEN)\b[:\s]*(.*)/i) ||
20
+ lines[i].match(/#\s*(TODO|FIXME|HACK|BUG|XXX|BROKEN)\b[:\s]*(.*)/i);
21
+ if (match) {
22
+ const tag = match[1].toUpperCase();
23
+ const msg = match[2]?.trim() || '';
24
+ if (tag === 'FIXME' || tag === 'BUG' || tag === 'HACK' || tag === 'BROKEN' || tag === 'XXX') {
25
+ findings.push({
26
+ ruleId: 'BUG-GEN-001', category: 'bugs', severity: 'low',
27
+ title: `${tag} comment: ${msg.slice(0, 80) || 'needs attention'}`,
28
+ description: `A ${tag} comment indicates known incomplete or broken code. Address it before shipping.`,
29
+ file: fp, line: i + 1, fix: null,
30
+ });
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return findings;
36
+ },
37
+ },
38
+ {
39
+ id: 'BUG-GEN-002',
40
+ category: 'bugs',
41
+ severity: 'high',
42
+ confidence: 'likely',
43
+ title: 'Division by zero without guard',
44
+ check({ files }) {
45
+ const findings = [];
46
+ for (const [fp, content] of files) {
47
+ if (!isSource(fp)) continue;
48
+ const lines = content.split('\n');
49
+ for (let i = 0; i < lines.length; i++) {
50
+ const line = lines[i];
51
+ // Skip comments, regex literals, imports, path strings
52
+ if (/^\s*\/\/|^\s*\*|^\s*\/\*|import\s|require\s*\(|['"`].*\/.*['"`]/.test(line)) continue;
53
+ // Match explicit arithmetic division: expr / variable
54
+ const divMatch = line.match(/\b(\w+)\s*\/\s*(\w+)\b/);
55
+ if (!divMatch) continue;
56
+ const [, , divisor] = divMatch;
57
+ // Must be a simple variable name, not a number, not in a comment
58
+ if (!/^[a-z][a-zA-Z0-9_]*$/.test(divisor)) continue;
59
+ if (/^(undefined|null|NaN|Infinity|true|false|this|Math|Date|JSON|Object|Array|String|Number)$/.test(divisor)) continue;
60
+ // Must be preceded by an arithmetic operator or assignment context (not a regex or path)
61
+ if (!/[=+\-*%(,]\s*\w+\s*\/\s*\w+/.test(line)) continue;
62
+ // Check for guards in surrounding context
63
+ const context = lines.slice(Math.max(0, i - 3), i + 1).join('\n');
64
+ if (/!==?\s*0|>\s*0|!= 0|> 0|\|\|\s*1|\?\?/.test(context)) continue;
65
+ if (new RegExp(`if\\s*\\(.*${divisor}`).test(context)) continue;
66
+ findings.push({
67
+ ruleId: 'BUG-GEN-002', category: 'bugs', severity: 'high',
68
+ title: `Potential division by zero — "${divisor}" not checked`,
69
+ description: 'This division could fail if the divisor is zero. Add a guard: if (divisor !== 0) or use a default value.',
70
+ file: fp, line: i + 1, fix: null,
71
+ });
72
+ }
73
+ }
74
+ return findings;
75
+ },
76
+ },
77
+ {
78
+ id: 'BUG-GEN-003',
79
+ category: 'bugs',
80
+ severity: 'high',
81
+ confidence: 'likely',
82
+ title: 'Hardcoded credentials in source code',
83
+ check({ files }) {
84
+ const findings = [];
85
+ for (const [fp, content] of files) {
86
+ if (!isSource(fp)) continue;
87
+ const lines = content.split('\n');
88
+ for (let i = 0; i < lines.length; i++) {
89
+ const line = lines[i];
90
+ // password = "..." or api_key = "..." or secret = "..."
91
+ if (/(?:password|passwd|api_?key|secret_?key|auth_?token|access_?token)\s*[:=]\s*['"][^'"]{4,}['"]/i.test(line)) {
92
+ if (!/(?:example|test|mock|fake|dummy|placeholder|xxx|your_|change_me|TODO)/i.test(line) && !fp.includes('test') && !fp.includes('example')) {
93
+ findings.push({
94
+ ruleId: 'BUG-GEN-003', category: 'bugs', severity: 'high',
95
+ title: 'Hardcoded credential in source code',
96
+ description: 'Credentials should never be hardcoded. Use environment variables or a secrets manager.',
97
+ file: fp, line: i + 1, fix: null,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return findings;
104
+ },
105
+ },
106
+ {
107
+ id: 'BUG-GEN-004',
108
+ category: 'bugs',
109
+ severity: 'high',
110
+ confidence: 'likely',
111
+ title: 'Commented-out code — dead code left in codebase',
112
+ check({ files }) {
113
+ const findings = [];
114
+ for (const [fp, content] of files) {
115
+ if (!isSource(fp)) continue;
116
+ const lines = content.split('\n');
117
+ let consecutiveCommented = 0;
118
+ let startLine = 0;
119
+ for (let i = 0; i < lines.length; i++) {
120
+ const line = lines[i].trim();
121
+ // Detect commented-out code (starts with // and looks like code)
122
+ const isCommentedCode = /^\/\/\s*(?:const|let|var|if|for|while|return|function|class|import|export|await|async|try|catch|switch)\b/.test(line) ||
123
+ /^#\s*(?:def|class|if|for|while|return|import|from|try|except|with)\b/.test(line);
124
+ if (isCommentedCode) {
125
+ if (consecutiveCommented === 0) startLine = i;
126
+ consecutiveCommented++;
127
+ } else {
128
+ if (consecutiveCommented >= 3) {
129
+ findings.push({
130
+ ruleId: 'BUG-GEN-004', category: 'bugs', severity: 'high',
131
+ title: `${consecutiveCommented} lines of commented-out code`,
132
+ description: 'Commented-out code creates noise and confusion. If it\'s needed, restore it. If not, delete it — git has the history.',
133
+ file: fp, line: startLine + 1, fix: null,
134
+ });
135
+ }
136
+ consecutiveCommented = 0;
137
+ }
138
+ }
139
+ }
140
+ return findings;
141
+ },
142
+ },
143
+ {
144
+ id: 'BUG-GEN-005',
145
+ category: 'bugs',
146
+ severity: 'low',
147
+ confidence: 'suggestion',
148
+ title: 'Magic number — unexplained numeric literal',
149
+ check({ files }) {
150
+ const findings = [];
151
+ const common = new Set([100, 200, 201, 204, 301, 302, 400, 401, 403, 404, 500, 502, 503, 1000, 1024, 2048, 4096, 8080, 3000, 5000, 8000, 8443, 9090, 65535, 86400, 3600, 60000, 30000, 10000, 5000, 1000]);
152
+ for (const [fp, content] of files) {
153
+ if (!isSource(fp)) continue;
154
+ const lines = content.split('\n');
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ // Skip comments, imports, test files, config files
158
+ if (/^\s*\/\/|^\s*\*|^\s*#|import\s|require\(/.test(line)) continue;
159
+ if (/test|spec|config|constant|fixture|mock|seed|migration/i.test(fp)) continue;
160
+ // Only flag numbers used inline in function calls or arithmetic (not in declarations, objects, arrays, or returns)
161
+ const match = line.match(/[^.\w'"$](\d{4,})[^.\w\d]/);
162
+ if (match) {
163
+ const num = parseInt(match[1], 10);
164
+ if (common.has(num)) continue;
165
+ // Skip if line has descriptive context
166
+ if (/(?:const|let|var|=|port|status|code|http|timeout|max|min|limit|size|width|height|delay|interval|version|offset|index|count|length|ms|seconds|minutes|hours|days|bytes|kb|mb|gb|pixels|px|margin|padding|duration|priority|weight|capacity)/i.test(line)) continue;
167
+ // Skip array/object literal positions, return statements
168
+ if (/^\s*\d|^\s*return\s|^\s*[\[{]/.test(line)) continue;
169
+ findings.push({
170
+ ruleId: 'BUG-GEN-005', category: 'bugs', severity: 'low',
171
+ title: `Magic number ${num} — extract to named constant`,
172
+ description: 'Unnamed numeric literals make code harder to understand and maintain. Give it a descriptive constant name.',
173
+ file: fp, line: i + 1, fix: null,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ return findings;
179
+ },
180
+ },
181
+ {
182
+ id: 'BUG-GEN-006',
183
+ category: 'bugs',
184
+ severity: 'high',
185
+ confidence: 'likely',
186
+ title: 'console.log/print left in production code',
187
+ check({ files }) {
188
+ const findings = [];
189
+ for (const [fp, content] of files) {
190
+ if (!isSource(fp)) continue;
191
+ if (fp.includes('test') || fp.includes('debug') || fp.includes('logger')) continue;
192
+ const lines = content.split('\n');
193
+ let debugCount = 0;
194
+ for (let i = 0; i < lines.length; i++) {
195
+ const line = lines[i].trim();
196
+ if (/^console\.log\s*\(/.test(line) || /^print\s*\((?!.*file\s*=)/.test(line)) {
197
+ debugCount++;
198
+ }
199
+ }
200
+ if (debugCount >= 3) {
201
+ findings.push({
202
+ ruleId: 'BUG-GEN-006', category: 'bugs', severity: 'high',
203
+ title: `${debugCount} debug print statements left in code`,
204
+ description: 'Multiple console.log/print statements suggest debugging leftovers. Replace with a proper logger or remove them.',
205
+ file: fp, fix: null,
206
+ });
207
+ }
208
+ }
209
+ return findings;
210
+ },
211
+ },
212
+ {
213
+ id: 'BUG-GEN-007',
214
+ category: 'bugs',
215
+ severity: 'high',
216
+ confidence: 'likely',
217
+ title: 'Duplicate function/method definition — second overwrites first',
218
+ check({ files }) {
219
+ const findings = [];
220
+ for (const [fp, content] of files) {
221
+ if (!isSource(fp)) continue;
222
+ const lines = content.split('\n');
223
+ const funcNames = {};
224
+ for (let i = 0; i < lines.length; i++) {
225
+ const jsMatch = lines[i].match(/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/);
226
+ const pyMatch = lines[i].match(/^\s*def\s+(\w+)\s*\(/);
227
+ const match = jsMatch || pyMatch;
228
+ if (match) {
229
+ const name = match[1];
230
+ if (funcNames[name] !== undefined) {
231
+ findings.push({
232
+ ruleId: 'BUG-GEN-007', category: 'bugs', severity: 'high',
233
+ title: `Duplicate function "${name}" — redefined on line ${i + 1}, first on line ${funcNames[name]}`,
234
+ description: 'The second definition silently overwrites the first. One of them is dead code or a copy-paste bug.',
235
+ file: fp, line: i + 1, fix: null,
236
+ });
237
+ } else {
238
+ funcNames[name] = i + 1;
239
+ }
240
+ }
241
+ }
242
+ }
243
+ return findings;
244
+ },
245
+ },
246
+ {
247
+ id: 'BUG-GEN-008',
248
+ category: 'bugs',
249
+ severity: 'high',
250
+ confidence: 'likely',
251
+ title: 'Regex without anchors used for validation — partial match passes',
252
+ check({ files }) {
253
+ const findings = [];
254
+ for (const [fp, content] of files) {
255
+ if (!isSource(fp)) continue;
256
+ const lines = content.split('\n');
257
+ for (let i = 0; i < lines.length; i++) {
258
+ const line = lines[i];
259
+ if (/\.test\s*\(/.test(line) || /\.match\s*\(/.test(line) || /re\.match\s*\(/.test(line)) {
260
+ // Check if regex is used for validation (near "valid", "email", "phone", etc.)
261
+ if (/(?:valid|email|phone|url|ip|date|format|check|verify)/i.test(line)) {
262
+ // Check if regex has anchors
263
+ const regexMatch = line.match(/\/([^/]+)\//) || line.match(/['"](\^?[^'"]+\$?)['"]/)
264
+ if (regexMatch && !regexMatch[1].startsWith('^') && !regexMatch[1].endsWith('$')) {
265
+ findings.push({
266
+ ruleId: 'BUG-GEN-008', category: 'bugs', severity: 'high',
267
+ title: 'Validation regex without anchors — partial matches pass',
268
+ description: 'Without ^ and $ anchors, "valid@email.com.evil" would pass an email regex. Add ^...$ anchors for validation.',
269
+ file: fp, line: i + 1, fix: null,
270
+ });
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ return findings;
277
+ },
278
+ },
279
+ {
280
+ id: 'BUG-GEN-009',
281
+ category: 'bugs',
282
+ severity: 'high',
283
+ confidence: 'likely',
284
+ title: 'Resource opened but never closed — file/connection leak',
285
+ check({ files }) {
286
+ const findings = [];
287
+ for (const [fp, content] of files) {
288
+ if (!isSource(fp)) continue;
289
+ const lines = content.split('\n');
290
+ for (let i = 0; i < lines.length; i++) {
291
+ const line = lines[i];
292
+ // JS: fs.openSync, createReadStream, createConnection without close
293
+ if (/(?:openSync|createReadStream|createWriteStream|createConnection|createServer)\s*\(/.test(line)) {
294
+ const rest = lines.slice(i, Math.min(lines.length, i + 30)).join('\n');
295
+ if (!rest.includes('.close') && !rest.includes('.end') && !rest.includes('.destroy') && !rest.includes('finally')) {
296
+ findings.push({
297
+ ruleId: 'BUG-GEN-009', category: 'bugs', severity: 'high',
298
+ title: 'Resource opened but never closed — potential leak',
299
+ description: 'Opened files, streams, and connections should be closed in a finally block or using using/with patterns.',
300
+ file: fp, line: i + 1, fix: null,
301
+ });
302
+ }
303
+ }
304
+ // Python: open() without with statement
305
+ if (fp.endsWith('.py') && /\w+\s*=\s*open\s*\(/.test(line)) {
306
+ if (!/^\s*with\b/.test(line)) {
307
+ const rest = lines.slice(i, Math.min(lines.length, i + 20)).join('\n');
308
+ if (!rest.includes('.close()') && !rest.includes('finally')) {
309
+ findings.push({
310
+ ruleId: 'BUG-GEN-009', category: 'bugs', severity: 'high',
311
+ title: 'File opened without "with" statement — may not be closed on error',
312
+ description: 'Use "with open(path) as f:" to ensure the file is closed even if an exception occurs.',
313
+ file: fp, line: i + 1, fix: null,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ return findings;
321
+ },
322
+ },
323
+ {
324
+ id: 'BUG-GEN-010',
325
+ category: 'bugs',
326
+ severity: 'medium',
327
+ confidence: 'likely',
328
+ title: 'Identical branches in if/else — copy-paste bug',
329
+ check({ files }) {
330
+ const findings = [];
331
+ for (const [fp, content] of files) {
332
+ if (!isSource(fp)) continue;
333
+ const lines = content.split('\n');
334
+ for (let i = 0; i < lines.length - 2; i++) {
335
+ // Simple heuristic: if block and else block have identical single-line bodies
336
+ if (/\bif\b.*\{?\s*$/.test(lines[i])) {
337
+ const ifBody = lines[i + 1]?.trim();
338
+ // Find the else
339
+ for (let j = i + 2; j < Math.min(lines.length, i + 6); j++) {
340
+ if (/\belse\b.*\{?\s*$/.test(lines[j]) && lines[j + 1]) {
341
+ const elseBody = lines[j + 1]?.trim();
342
+ if (ifBody && elseBody && ifBody === elseBody && ifBody.length > 5 && !ifBody.startsWith('//') && !ifBody.startsWith('#')) {
343
+ findings.push({
344
+ ruleId: 'BUG-GEN-010', category: 'bugs', severity: 'medium',
345
+ title: 'if/else branches have identical code — likely copy-paste bug',
346
+ description: 'Both branches do the same thing, making the condition useless. Fix one branch or remove the condition.',
347
+ file: fp, line: i + 1, fix: null,
348
+ });
349
+ }
350
+ break;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ return findings;
357
+ },
358
+ },
359
+ ];
360
+
361
+ export default rules;
@@ -0,0 +1,279 @@
1
+ // Bug detection: Go-specific patterns — concurrency, error handling, common traps
2
+ function isGo(f) { return f.endsWith('.go'); }
3
+
4
+ const rules = [
5
+ {
6
+ id: 'BUG-GO-001',
7
+ category: 'bugs',
8
+ severity: 'high',
9
+ confidence: 'likely',
10
+ title: 'Goroutine leak — goroutine started without cancellation',
11
+ check({ files }) {
12
+ const findings = [];
13
+ for (const [fp, content] of files) {
14
+ if (!isGo(fp)) continue;
15
+ const lines = content.split('\n');
16
+ for (let i = 0; i < lines.length; i++) {
17
+ if (/^\s*go\s+func\s*\(/.test(lines[i]) || /^\s*go\s+\w+\(/.test(lines[i])) {
18
+ // Check surrounding context for context/cancel/done
19
+ const context = lines.slice(Math.max(0, i - 5), Math.min(i + 15, lines.length)).join('\n');
20
+ if (!/context|ctx|cancel|Done\(\)|select\s*\{|time\.After|timer/.test(context)) {
21
+ findings.push({
22
+ ruleId: 'BUG-GO-001', category: 'bugs', severity: 'high',
23
+ title: 'Goroutine started without context or cancellation mechanism',
24
+ description: 'Goroutines without context.Context or done channels can leak forever. Pass a context and select on ctx.Done().',
25
+ file: fp, line: i + 1, fix: null,
26
+ });
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return findings;
32
+ },
33
+ },
34
+ {
35
+ id: 'BUG-GO-002',
36
+ category: 'bugs',
37
+ severity: 'high',
38
+ confidence: 'definite',
39
+ title: 'Error not checked after function call',
40
+ check({ files }) {
41
+ const findings = [];
42
+ for (const [fp, content] of files) {
43
+ if (!isGo(fp)) continue;
44
+ const lines = content.split('\n');
45
+ for (let i = 0; i < lines.length; i++) {
46
+ // result, _ := someFunc() or someFunc() without checking err
47
+ if (/\w+\s*,\s*_\s*:?=\s*\w+/.test(lines[i]) && !/range\b|import/.test(lines[i])) {
48
+ // Check if the ignored value is likely an error
49
+ const funcCall = lines[i].match(/\w+\s*,\s*_\s*:?=\s*(\w+)/);
50
+ if (funcCall) {
51
+ findings.push({
52
+ ruleId: 'BUG-GO-002', category: 'bugs', severity: 'high',
53
+ title: 'Error return value discarded with _ — errors must be handled',
54
+ description: 'Ignoring errors with _ hides failures. Always check: if err != nil { return err }.',
55
+ file: fp, line: i + 1, fix: null,
56
+ });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ return findings;
62
+ },
63
+ },
64
+ {
65
+ id: 'BUG-GO-003',
66
+ category: 'bugs',
67
+ severity: 'high',
68
+ confidence: 'definite',
69
+ title: 'defer inside loop — deferred calls pile up',
70
+ check({ files }) {
71
+ const findings = [];
72
+ for (const [fp, content] of files) {
73
+ if (!isGo(fp)) continue;
74
+ const lines = content.split('\n');
75
+ let inLoop = 0;
76
+ for (let i = 0; i < lines.length; i++) {
77
+ if (/^\s*for\b/.test(lines[i])) inLoop = 20;
78
+ if (inLoop > 0) {
79
+ inLoop--;
80
+ if (/^\s*defer\b/.test(lines[i])) {
81
+ findings.push({
82
+ ruleId: 'BUG-GO-003', category: 'bugs', severity: 'high',
83
+ title: 'defer inside loop — deferred calls accumulate until function returns',
84
+ description: 'defer runs at function exit, not loop iteration end. In a loop, deferred calls pile up and may exhaust resources. Close resources explicitly in the loop body.',
85
+ file: fp, line: i + 1, fix: null,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return findings;
92
+ },
93
+ },
94
+ {
95
+ id: 'BUG-GO-004',
96
+ category: 'bugs',
97
+ severity: 'critical',
98
+ confidence: 'likely',
99
+ title: 'Race condition — shared variable in goroutine without sync',
100
+ check({ files }) {
101
+ const findings = [];
102
+ for (const [fp, content] of files) {
103
+ if (!isGo(fp)) continue;
104
+ const lines = content.split('\n');
105
+ for (let i = 0; i < lines.length; i++) {
106
+ if (/^\s*go\s+func\s*\(/.test(lines[i])) {
107
+ // Check if the goroutine captures variables from outer scope
108
+ let goBlock = '';
109
+ let braceDepth = 0;
110
+ for (let j = i; j < Math.min(i + 20, lines.length); j++) {
111
+ goBlock += lines[j] + '\n';
112
+ braceDepth += (lines[j].match(/\{/g) || []).length - (lines[j].match(/\}/g) || []).length;
113
+ if (braceDepth <= 0 && j > i) break;
114
+ }
115
+ // Check for writes to captured vars without mutex/atomic/channel
116
+ const hasMutex = /\.Lock\(\)|\.Unlock\(\)|atomic\.|sync\.|<-/.test(goBlock);
117
+ const writesShared = /\w+\s*[+\-*/]?=(?!=)/.test(goBlock) && !/^\s*\w+\s*:=/.test(goBlock);
118
+ if (!hasMutex && writesShared && /\b(count|total|result|sum|data|shared|state|cache)\b/.test(goBlock)) {
119
+ findings.push({
120
+ ruleId: 'BUG-GO-004', category: 'bugs', severity: 'high',
121
+ title: 'Goroutine writes to shared variable without synchronization',
122
+ description: 'Concurrent writes without mutex, atomic, or channels cause data races. Use sync.Mutex, atomic operations, or channels.',
123
+ file: fp, line: i + 1, fix: null,
124
+ });
125
+ }
126
+ }
127
+ }
128
+ }
129
+ return findings;
130
+ },
131
+ },
132
+ {
133
+ id: 'BUG-GO-005',
134
+ category: 'bugs',
135
+ severity: 'high',
136
+ confidence: 'definite',
137
+ title: 'Loop variable captured in goroutine closure',
138
+ check({ files }) {
139
+ const findings = [];
140
+ for (const [fp, content] of files) {
141
+ if (!isGo(fp)) continue;
142
+ const lines = content.split('\n');
143
+ for (let i = 0; i < lines.length; i++) {
144
+ if (/^\s*for\s+.*(?::=\s*range|;\s*\w+\s*<)/.test(lines[i])) {
145
+ const loopVar = lines[i].match(/for\s+(?:_\s*,\s*)?(\w+)/)?.[1];
146
+ if (!loopVar) continue;
147
+ for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
148
+ if (/^\s*go\s+func\s*\(/.test(lines[j])) {
149
+ // Check if goroutine uses loop var without passing as parameter
150
+ let goBlock = '';
151
+ for (let k = j; k < Math.min(j + 10, lines.length); k++) {
152
+ goBlock += lines[k] + '\n';
153
+ }
154
+ const usesVar = new RegExp(`\\b${loopVar}\\b`).test(goBlock);
155
+ // Check if passed as argument: go func(v Type) { ... }(loopVar)
156
+ const passedAsArg = new RegExp(`\\}\\s*\\(\\s*${loopVar}\\s*\\)`).test(goBlock);
157
+ if (usesVar && !passedAsArg) {
158
+ findings.push({
159
+ ruleId: 'BUG-GO-005', category: 'bugs', severity: 'high',
160
+ title: `Loop variable "${loopVar}" captured by goroutine — all goroutines see last value`,
161
+ description: 'In Go <1.22, loop variables are shared. Pass the variable as a goroutine argument: go func(v T) { ... }(loopVar).',
162
+ file: fp, line: j + 1, fix: null,
163
+ });
164
+ }
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ return findings;
172
+ },
173
+ },
174
+ {
175
+ id: 'BUG-GO-006',
176
+ category: 'bugs',
177
+ severity: 'high',
178
+ confidence: 'likely',
179
+ title: 'HTTP response body not closed',
180
+ check({ files }) {
181
+ const findings = [];
182
+ for (const [fp, content] of files) {
183
+ if (!isGo(fp)) continue;
184
+ const lines = content.split('\n');
185
+ for (let i = 0; i < lines.length; i++) {
186
+ if (/http\.(Get|Post|Do|Head)\s*\(/.test(lines[i]) || /client\.(Get|Post|Do)\s*\(/.test(lines[i])) {
187
+ let block = '';
188
+ for (let j = i; j < Math.min(i + 10, lines.length); j++) {
189
+ block += lines[j] + '\n';
190
+ }
191
+ if (!/resp\.Body\.Close|defer.*Body\.Close|ioutil\.ReadAll|io\.ReadAll/.test(block)) {
192
+ findings.push({
193
+ ruleId: 'BUG-GO-006', category: 'bugs', severity: 'high',
194
+ title: 'HTTP response body not closed — leaks connections',
195
+ description: 'Always close HTTP response bodies: defer resp.Body.Close(). Unclosed bodies leak TCP connections.',
196
+ file: fp, line: i + 1, fix: null,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ return findings;
203
+ },
204
+ },
205
+ {
206
+ id: 'BUG-GO-007',
207
+ category: 'bugs',
208
+ severity: 'medium',
209
+ confidence: 'definite',
210
+ title: 'nil map assignment — panics at runtime',
211
+ check({ files }) {
212
+ const findings = [];
213
+ for (const [fp, content] of files) {
214
+ if (!isGo(fp)) continue;
215
+ const lines = content.split('\n');
216
+ for (let i = 0; i < lines.length; i++) {
217
+ // var m map[...]... without make()
218
+ if (/^\s*var\s+(\w+)\s+map\[/.test(lines[i])) {
219
+ const mapVar = lines[i].match(/var\s+(\w+)/)[1];
220
+ // Check if make() is called before first assignment
221
+ let madeOrAssigned = false;
222
+ for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
223
+ if (new RegExp(`${mapVar}\\s*=\\s*(?:make|map)\\b`).test(lines[j])) {
224
+ madeOrAssigned = true;
225
+ break;
226
+ }
227
+ if (new RegExp(`${mapVar}\\s*\\[`).test(lines[j]) && /=/.test(lines[j])) {
228
+ if (!madeOrAssigned) {
229
+ findings.push({
230
+ ruleId: 'BUG-GO-007', category: 'bugs', severity: 'medium',
231
+ title: `Nil map "${mapVar}" assigned to — panics at runtime`,
232
+ description: 'A declared but uninitialized map is nil. Assigning to a nil map causes a runtime panic. Use make(map[K]V) first.',
233
+ file: fp, line: j + 1, fix: null,
234
+ });
235
+ }
236
+ break;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ return findings;
243
+ },
244
+ },
245
+ {
246
+ id: 'BUG-GO-008',
247
+ category: 'bugs',
248
+ severity: 'high',
249
+ confidence: 'likely',
250
+ title: 'WaitGroup used incorrectly',
251
+ check({ files }) {
252
+ const findings = [];
253
+ for (const [fp, content] of files) {
254
+ if (!isGo(fp)) continue;
255
+ const lines = content.split('\n');
256
+ for (let i = 0; i < lines.length; i++) {
257
+ // wg.Add inside goroutine instead of before it
258
+ if (/^\s*go\s+func/.test(lines[i])) {
259
+ let goBlock = '';
260
+ for (let j = i; j < Math.min(i + 10, lines.length); j++) {
261
+ goBlock += lines[j] + '\n';
262
+ }
263
+ if (/wg\.Add\s*\(/.test(goBlock)) {
264
+ findings.push({
265
+ ruleId: 'BUG-GO-008', category: 'bugs', severity: 'high',
266
+ title: 'WaitGroup.Add() called inside goroutine — race with Wait()',
267
+ description: 'wg.Add() must be called before the goroutine starts, not inside it. Otherwise Wait() may return before Add() runs.',
268
+ file: fp, line: i + 1, fix: null,
269
+ });
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return findings;
275
+ },
276
+ },
277
+ ];
278
+
279
+ export default rules;