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,242 @@
1
+ // Bug detection: Next.js/SSR patterns AI generators get wrong
2
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx'];
3
+ function isJSX(f) { return JS_EXT.some(e => f.endsWith(e)); }
4
+
5
+ const rules = [
6
+ {
7
+ id: 'BUG-NEXT-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'likely',
11
+ title: 'Client-side API (window/document/localStorage) used in server component',
12
+ check({ files }) {
13
+ const findings = [];
14
+ for (const [fp, content] of files) {
15
+ if (!isJSX(fp)) continue;
16
+ // Next.js app router: files without "use client" are server components
17
+ const isAppRouter = /[\/\\]app[\/\\]/.test(fp) || /\/app\\/.test(fp);
18
+ if (!isAppRouter) continue;
19
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
20
+ if (hasUseClient) continue;
21
+ const lines = content.split('\n');
22
+ for (let i = 0; i < lines.length; i++) {
23
+ if (/\b(window|document|localStorage|sessionStorage|navigator|location\.href|addEventListener)\b/.test(lines[i]) && !/typeof\s+(window|document)/.test(lines[i]) && !/\/\//.test(lines[i].split(/window|document/)[0])) {
24
+ findings.push({
25
+ ruleId: 'BUG-NEXT-001', category: 'bugs', severity: 'high',
26
+ title: 'Browser API used in server component — will crash at build time',
27
+ description: 'This file is in the app/ directory without "use client". Browser APIs (window, document, localStorage) are not available on the server. Add "use client" or wrap in useEffect.',
28
+ file: fp, line: i + 1, fix: null,
29
+ });
30
+ }
31
+ }
32
+ }
33
+ return findings;
34
+ },
35
+ },
36
+ {
37
+ id: 'BUG-NEXT-002',
38
+ category: 'bugs',
39
+ severity: 'high',
40
+ confidence: 'likely',
41
+ title: 'useState/useEffect in server component',
42
+ check({ files }) {
43
+ const findings = [];
44
+ for (const [fp, content] of files) {
45
+ if (!isJSX(fp)) continue;
46
+ const isAppRouter = /[\/\\]app[\/\\]/.test(fp);
47
+ if (!isAppRouter) continue;
48
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
49
+ if (hasUseClient) continue;
50
+ const lines = content.split('\n');
51
+ for (let i = 0; i < lines.length; i++) {
52
+ if (/\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\s*\(/.test(lines[i])) {
53
+ const hook = lines[i].match(/(use(?:State|Effect|Ref|Callback|Memo|Reducer|Context))/)[1];
54
+ findings.push({
55
+ ruleId: 'BUG-NEXT-002', category: 'bugs', severity: 'high',
56
+ title: `${hook} in server component — hooks only work in client components`,
57
+ description: `Add "use client" at the top of this file to use ${hook}. Server components cannot use React hooks.`,
58
+ file: fp, line: i + 1, fix: null,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return findings;
64
+ },
65
+ },
66
+ {
67
+ id: 'BUG-NEXT-003',
68
+ category: 'bugs',
69
+ severity: 'medium',
70
+ confidence: 'likely',
71
+ title: '"use client" on a component that doesn\'t need it',
72
+ check({ files }) {
73
+ const findings = [];
74
+ for (const [fp, content] of files) {
75
+ if (!isJSX(fp)) continue;
76
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
77
+ if (!hasUseClient) continue;
78
+ // Check if it actually uses client features
79
+ const needsClient = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext|onClick|onChange|onSubmit|onKeyDown|onMouseDown|onFocus|onBlur|addEventListener|window\.|document\.|localStorage|sessionStorage|navigator)\b/.test(content);
80
+ if (!needsClient) {
81
+ findings.push({
82
+ ruleId: 'BUG-NEXT-003', category: 'bugs', severity: 'medium',
83
+ title: '"use client" but no client features used — unnecessary client boundary',
84
+ description: 'This component has "use client" but doesn\'t use hooks, event handlers, or browser APIs. Removing it enables server rendering, reducing JS sent to the browser.',
85
+ file: fp, line: 1, fix: null,
86
+ });
87
+ }
88
+ }
89
+ return findings;
90
+ },
91
+ },
92
+ {
93
+ id: 'BUG-NEXT-004',
94
+ category: 'bugs',
95
+ severity: 'high',
96
+ confidence: 'likely',
97
+ title: 'fetch() in client component instead of server',
98
+ check({ files }) {
99
+ const findings = [];
100
+ for (const [fp, content] of files) {
101
+ if (!isJSX(fp)) continue;
102
+ if (!/[\/\\]app[\/\\]/.test(fp)) continue;
103
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
104
+ if (!hasUseClient) continue;
105
+ // Client component doing data fetching in useEffect
106
+ if (/useEffect[\s\S]*fetch\s*\(/.test(content) && !/useSWR|useQuery|tanstack|swr/.test(content)) {
107
+ findings.push({
108
+ ruleId: 'BUG-NEXT-004', category: 'bugs', severity: 'high',
109
+ title: 'Data fetching in client component useEffect — use server component or SWR',
110
+ description: 'Fetching in useEffect causes loading waterfalls and shows spinners. In Next.js, fetch data in server components (no "use client") or use SWR/React Query for caching.',
111
+ file: fp, line: 1, fix: null,
112
+ });
113
+ }
114
+ }
115
+ return findings;
116
+ },
117
+ },
118
+ {
119
+ id: 'BUG-NEXT-005',
120
+ category: 'bugs',
121
+ severity: 'high',
122
+ confidence: 'definite',
123
+ title: 'Secrets exposed to client bundle',
124
+ check({ files }) {
125
+ const findings = [];
126
+ for (const [fp, content] of files) {
127
+ if (!isJSX(fp)) continue;
128
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
129
+ const lines = content.split('\n');
130
+ for (let i = 0; i < lines.length; i++) {
131
+ // NEXT_PUBLIC_ env vars with secret-like names
132
+ if (/process\.env\.NEXT_PUBLIC_.*(?:SECRET|PRIVATE|KEY|TOKEN|PASSWORD|CREDENTIAL)/i.test(lines[i]) && !/PUBLIC_KEY|PUBLISHABLE/.test(lines[i])) {
133
+ findings.push({
134
+ ruleId: 'BUG-NEXT-005', category: 'bugs', severity: 'high',
135
+ title: 'Secret in NEXT_PUBLIC_ env var — exposed to client browser',
136
+ description: 'NEXT_PUBLIC_ variables are embedded in the client JS bundle and visible to anyone. Secrets must NOT use the NEXT_PUBLIC_ prefix.',
137
+ file: fp, line: i + 1, fix: null,
138
+ });
139
+ }
140
+ // Server-only env var in client component
141
+ if (hasUseClient && /process\.env\.(?!NEXT_PUBLIC_)\w+/.test(lines[i]) && !/\/\//.test(lines[i].split('process.env')[0])) {
142
+ findings.push({
143
+ ruleId: 'BUG-NEXT-005', category: 'bugs', severity: 'high',
144
+ title: 'Server env var in client component — will be undefined at runtime',
145
+ description: 'Only NEXT_PUBLIC_ env vars are available in client components. This will be undefined. Move to a server component or API route.',
146
+ file: fp, line: i + 1, fix: null,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ return findings;
152
+ },
153
+ },
154
+ {
155
+ id: 'BUG-NEXT-006',
156
+ category: 'bugs',
157
+ severity: 'medium',
158
+ confidence: 'likely',
159
+ title: 'Missing loading.tsx or error.tsx in route',
160
+ check({ files }) {
161
+ const findings = [];
162
+ const routeDirs = new Set();
163
+ for (const [fp] of files) {
164
+ if (/[\/\\]app[\/\\].*\/page\.(tsx|jsx|ts|js)$/.test(fp)) {
165
+ routeDirs.add(fp.replace(/\/page\.\w+$/, ''));
166
+ }
167
+ }
168
+ for (const dir of routeDirs) {
169
+ const hasLoading = [...files.keys()].some(f => f.startsWith(dir + '/loading.'));
170
+ const hasError = [...files.keys()].some(f => f.startsWith(dir + '/error.'));
171
+ if (!hasLoading && !hasError) {
172
+ // Only flag if the page has data fetching
173
+ const pagePath = [...files.keys()].find(f => f.startsWith(dir + '/page.'));
174
+ if (pagePath) {
175
+ const content = files.get(pagePath);
176
+ if (/fetch\(|prisma\.|db\.|getServerSession|await/.test(content)) {
177
+ findings.push({
178
+ ruleId: 'BUG-NEXT-006', category: 'bugs', severity: 'medium',
179
+ title: `Route "${dir}" has async data but no loading.tsx or error.tsx`,
180
+ description: 'Without loading.tsx, users see a blank page during data fetching. Without error.tsx, errors crash the entire page. Add both for good UX.',
181
+ file: pagePath, line: 1, fix: null,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return findings;
188
+ },
189
+ },
190
+ {
191
+ id: 'BUG-NEXT-007',
192
+ category: 'bugs',
193
+ severity: 'medium',
194
+ confidence: 'likely',
195
+ title: 'Missing metadata export in page',
196
+ check({ files }) {
197
+ const findings = [];
198
+ for (const [fp, content] of files) {
199
+ if (!/[\/\\]app[\/\\].*\/page\.(tsx|jsx|ts|js)$/.test(fp)) continue;
200
+ if (!/export\s+(const\s+metadata|function\s+generateMetadata|async\s+function\s+generateMetadata)/.test(content)) {
201
+ findings.push({
202
+ ruleId: 'BUG-NEXT-007', category: 'bugs', severity: 'medium',
203
+ title: 'Page without metadata export — missing title/description for SEO',
204
+ description: 'Export a metadata object or generateMetadata function for proper SEO. Without it, the page has no title, description, or social previews.',
205
+ file: fp, line: 1, fix: null,
206
+ });
207
+ }
208
+ }
209
+ return findings;
210
+ },
211
+ },
212
+ {
213
+ id: 'BUG-NEXT-008',
214
+ category: 'bugs',
215
+ severity: 'high',
216
+ confidence: 'likely',
217
+ title: 'Importing server-only module in client component',
218
+ check({ files }) {
219
+ const findings = [];
220
+ for (const [fp, content] of files) {
221
+ if (!isJSX(fp)) continue;
222
+ const hasUseClient = /^['"]use client['"]/.test(content.trim());
223
+ if (!hasUseClient) continue;
224
+ const lines = content.split('\n');
225
+ for (let i = 0; i < lines.length; i++) {
226
+ // Importing server-only packages
227
+ if (/import.*from\s+['"](?:server-only|fs|path|crypto|child_process|net|dns|os|cluster)['"]/.test(lines[i])) {
228
+ findings.push({
229
+ ruleId: 'BUG-NEXT-008', category: 'bugs', severity: 'high',
230
+ title: 'Server-only module imported in client component — build will fail',
231
+ description: 'Node.js modules (fs, path, crypto) and server-only packages cannot be imported in client components.',
232
+ file: fp, line: i + 1, fix: null,
233
+ });
234
+ }
235
+ }
236
+ }
237
+ return findings;
238
+ },
239
+ },
240
+ ];
241
+
242
+ export default rules;
@@ -0,0 +1,120 @@
1
+ // Auto-fixable Next.js patterns
2
+ const JSX_EXT = ['.jsx', '.tsx', '.js', '.ts'];
3
+ function isJSX(f) { return JSX_EXT.some(e => f.endsWith(e)); }
4
+
5
+ const rules = [
6
+ {
7
+ id: 'BUG-NXFIX-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'definite',
11
+ title: 'Add "use client" for component using hooks',
12
+ check({ files }) {
13
+ const findings = [];
14
+ for (const [fp, content] of files) {
15
+ if (!isJSX(fp)) continue;
16
+ if (!/[\/\\]app[\/\\]/.test(fp)) continue;
17
+ if (/^['"]use client['"]/.test(content.trim())) continue;
18
+ // Has hooks but no "use client"
19
+ if (/\b(useState|useEffect|useRef|useCallback|useMemo|useReducer)\s*\(/.test(content)) {
20
+ const hook = content.match(/(useState|useEffect|useRef|useCallback|useMemo|useReducer)/)[1];
21
+ findings.push({
22
+ ruleId: 'BUG-NXFIX-001', category: 'bugs', severity: 'high',
23
+ title: `Add "use client" — uses ${hook} but missing client directive`,
24
+ file: fp, line: 1,
25
+ fix: {
26
+ type: 'insert',
27
+ position: 'start',
28
+ content: '"use client";\n\n',
29
+ },
30
+ });
31
+ }
32
+ }
33
+ return findings;
34
+ },
35
+ },
36
+ {
37
+ id: 'BUG-NXFIX-002',
38
+ category: 'bugs',
39
+ severity: 'medium',
40
+ confidence: 'definite',
41
+ title: 'Remove unnecessary "use client"',
42
+ check({ files }) {
43
+ const findings = [];
44
+ for (const [fp, content] of files) {
45
+ if (!isJSX(fp)) continue;
46
+ const firstLine = content.trim().split('\n')[0];
47
+ if (!/^['"]use client['"]/.test(firstLine)) continue;
48
+ const needsClient = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext|onClick|onChange|onSubmit|onKeyDown|onMouseDown|onFocus|onBlur|addEventListener|window\.|document\.|localStorage|sessionStorage|navigator)\b/.test(content);
49
+ if (!needsClient) {
50
+ findings.push({
51
+ ruleId: 'BUG-NXFIX-002', category: 'bugs', severity: 'medium',
52
+ title: 'Remove "use client" — no client features used',
53
+ file: fp, line: 1,
54
+ fix: {
55
+ type: 'replace',
56
+ old: firstLine,
57
+ new: '',
58
+ },
59
+ });
60
+ }
61
+ }
62
+ return findings;
63
+ },
64
+ },
65
+ {
66
+ id: 'BUG-NXFIX-003',
67
+ category: 'bugs',
68
+ severity: 'medium',
69
+ confidence: 'definite',
70
+ title: '<img> → Next.js <Image> component',
71
+ check({ files }) {
72
+ const findings = [];
73
+ for (const [fp, content] of files) {
74
+ if (!isJSX(fp)) continue;
75
+ if (!/next/.test(content) && !/[\/\\]app[\/\\]/.test(fp)) continue;
76
+ const lines = content.split('\n');
77
+ for (let i = 0; i < lines.length; i++) {
78
+ if (/<img\s/.test(lines[i]) && !/<Image\b/.test(lines[i])) {
79
+ findings.push({
80
+ ruleId: 'BUG-NXFIX-003', category: 'bugs', severity: 'medium',
81
+ title: '<img> → use Next.js <Image> for automatic optimization',
82
+ file: fp, line: i + 1,
83
+ fix: { suggestion: 'Replace <img> with Next.js <Image> component from "next/image" for automatic lazy loading, responsive sizing, and WebP conversion.' },
84
+ });
85
+ }
86
+ }
87
+ }
88
+ return findings;
89
+ },
90
+ },
91
+ {
92
+ id: 'BUG-NXFIX-004',
93
+ category: 'bugs',
94
+ severity: 'medium',
95
+ confidence: 'definite',
96
+ title: '<a> → Next.js <Link> component',
97
+ check({ files }) {
98
+ const findings = [];
99
+ for (const [fp, content] of files) {
100
+ if (!isJSX(fp)) continue;
101
+ if (!/next/.test(content) && !/[\/\\]app[\/\\]/.test(fp)) continue;
102
+ const lines = content.split('\n');
103
+ for (let i = 0; i < lines.length; i++) {
104
+ // <a href="/internal"> — internal link not using Next.js Link
105
+ if (/<a\b[^>]*href\s*=\s*['"]\/[^'"]*['"]/.test(lines[i]) && !/<Link\b/.test(lines[i])) {
106
+ findings.push({
107
+ ruleId: 'BUG-NXFIX-004', category: 'bugs', severity: 'medium',
108
+ title: '<a href="/..."> → use Next.js <Link> for client-side navigation',
109
+ file: fp, line: i + 1,
110
+ fix: { suggestion: 'Replace <a> with Next.js <Link> from "next/link" for client-side navigation (faster transitions, prefetching).' },
111
+ });
112
+ }
113
+ }
114
+ }
115
+ return findings;
116
+ },
117
+ },
118
+ ];
119
+
120
+ export default rules;
@@ -0,0 +1,178 @@
1
+ // Auto-fixable Node.js/backend bug 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-NFIX-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'definite',
11
+ title: 'res.send without return → return res.send',
12
+ check({ files }) {
13
+ const findings = [];
14
+ for (const [fp, content] of files) {
15
+ if (!isJS(fp)) continue;
16
+ if (!/express|router/.test(content)) continue;
17
+ const lines = content.split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ // res.send/json/status without return, followed by more res calls
20
+ if (/^\s+res\.(send|json|status)\s*\(/.test(lines[i]) && !/return\s+res/.test(lines[i]) && !/^\s*return\b/.test(lines[i])) {
21
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
22
+ if (/res\.(send|json|status|end|redirect)/.test(lines[j])) {
23
+ findings.push({
24
+ ruleId: 'BUG-NFIX-001', category: 'bugs', severity: 'high',
25
+ title: 'Add return before res.send() to prevent double response',
26
+ file: fp, line: i + 1,
27
+ fix: {
28
+ type: 'replace',
29
+ old: lines[i],
30
+ new: lines[i].replace(/^(\s*)res\./, '$1return res.'),
31
+ },
32
+ });
33
+ break;
34
+ }
35
+ if (/return\b|\}/.test(lines[j])) break;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return findings;
41
+ },
42
+ },
43
+ {
44
+ id: 'BUG-NFIX-002',
45
+ category: 'bugs',
46
+ severity: 'medium',
47
+ confidence: 'definite',
48
+ title: 'readFileSync → readFile (async)',
49
+ check({ files }) {
50
+ const findings = [];
51
+ for (const [fp, content] of files) {
52
+ if (!isJS(fp)) continue;
53
+ const isServerFile = /express|fastify|http\.createServer|app\.(get|post)|router/.test(content);
54
+ if (!isServerFile) continue;
55
+ const lines = content.split('\n');
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const match = lines[i].match(/fs\.readFileSync\s*\(([^)]+)\)/);
58
+ if (match) {
59
+ findings.push({
60
+ ruleId: 'BUG-NFIX-002', category: 'bugs', severity: 'medium',
61
+ title: 'readFileSync → await fs.promises.readFile in server code',
62
+ file: fp, line: i + 1,
63
+ fix: {
64
+ type: 'replace',
65
+ old: `fs.readFileSync(${match[1]})`,
66
+ new: `await fs.promises.readFile(${match[1]})`,
67
+ },
68
+ });
69
+ }
70
+ }
71
+ }
72
+ return findings;
73
+ },
74
+ },
75
+ {
76
+ id: 'BUG-NFIX-003',
77
+ category: 'bugs',
78
+ severity: 'medium',
79
+ confidence: 'definite',
80
+ title: 'callback(err) without return → return callback(err)',
81
+ check({ files }) {
82
+ const findings = [];
83
+ for (const [fp, content] of files) {
84
+ if (!isJS(fp)) continue;
85
+ const lines = content.split('\n');
86
+ for (let i = 0; i < lines.length; i++) {
87
+ if (/^\s+(callback|cb|next|done)\s*\(\s*(err|error|e|null)\s*\)\s*;?\s*$/.test(lines[i]) && !/return/.test(lines[i])) {
88
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
89
+ const cbName = lines[i].match(/(callback|cb|next|done)/)[1];
90
+ if (new RegExp(`\\b${cbName}\\b`).test(lines[j])) {
91
+ findings.push({
92
+ ruleId: 'BUG-NFIX-003', category: 'bugs', severity: 'medium',
93
+ title: `Add return before ${cbName}() to prevent double callback`,
94
+ file: fp, line: i + 1,
95
+ fix: {
96
+ type: 'replace',
97
+ old: lines[i],
98
+ new: lines[i].replace(/^(\s*)(callback|cb|next|done)/, '$1return $2'),
99
+ },
100
+ });
101
+ break;
102
+ }
103
+ if (/return\b|\}/.test(lines[j])) break;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return findings;
109
+ },
110
+ },
111
+ {
112
+ id: 'BUG-NFIX-004',
113
+ category: 'bugs',
114
+ severity: 'medium',
115
+ confidence: 'definite',
116
+ title: 'JSON.stringify(req/res) → serialize specific properties',
117
+ check({ files }) {
118
+ const findings = [];
119
+ for (const [fp, content] of files) {
120
+ if (!isJS(fp)) continue;
121
+ const lines = content.split('\n');
122
+ for (let i = 0; i < lines.length; i++) {
123
+ const match = lines[i].match(/JSON\.stringify\s*\(\s*(req|res)\s*\)/);
124
+ if (match) {
125
+ const obj = match[1];
126
+ const replacement = obj === 'req'
127
+ ? `JSON.stringify({ method: ${obj}.method, url: ${obj}.url, headers: ${obj}.headers })`
128
+ : `JSON.stringify({ statusCode: ${obj}.statusCode })`;
129
+ findings.push({
130
+ ruleId: 'BUG-NFIX-004', category: 'bugs', severity: 'medium',
131
+ title: `JSON.stringify(${obj}) → serialize specific properties to avoid circular reference`,
132
+ file: fp, line: i + 1,
133
+ fix: {
134
+ type: 'replace',
135
+ old: `JSON.stringify(${obj})`,
136
+ new: replacement,
137
+ },
138
+ });
139
+ }
140
+ }
141
+ }
142
+ return findings;
143
+ },
144
+ },
145
+ {
146
+ id: 'BUG-NFIX-005',
147
+ category: 'bugs',
148
+ severity: 'medium',
149
+ confidence: 'definite',
150
+ title: 'process.env.X → process.env.X || default for non-critical vars',
151
+ check({ files }) {
152
+ const findings = [];
153
+ for (const [fp, content] of files) {
154
+ if (!isJS(fp)) continue;
155
+ const lines = content.split('\n');
156
+ for (let i = 0; i < lines.length; i++) {
157
+ // const PORT = process.env.PORT (without fallback)
158
+ const match = lines[i].match(/(?:const|let|var)\s+PORT\s*=\s*process\.env\.PORT\s*;?\s*$/);
159
+ if (match) {
160
+ findings.push({
161
+ ruleId: 'BUG-NFIX-005', category: 'bugs', severity: 'medium',
162
+ title: 'PORT without fallback → add default value',
163
+ file: fp, line: i + 1,
164
+ fix: {
165
+ type: 'replace',
166
+ old: 'process.env.PORT',
167
+ new: 'process.env.PORT || 3000',
168
+ },
169
+ });
170
+ }
171
+ }
172
+ }
173
+ return findings;
174
+ },
175
+ },
176
+ ];
177
+
178
+ export default rules;