getdoorman 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// Bug detection: React/JSX patterns that Claude generates incorrectly
|
|
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-REACT-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'setState inside useEffect without dependency array',
|
|
12
|
+
description: 'Calling setState in useEffect without deps causes infinite re-render loops.',
|
|
13
|
+
check({ files }) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
for (const [fp, content] of files) {
|
|
16
|
+
if (!isJSX(fp)) continue;
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
if (/useEffect\(\s*\(\)\s*=>\s*\{/.test(lines[i]) || /useEffect\(\s*(?:async\s*)?\(\)\s*=>/.test(lines[i])) {
|
|
20
|
+
// Look for setState in the next 15 lines, and check if there's a dep array
|
|
21
|
+
let hasSetState = false;
|
|
22
|
+
let hasDepArray = false;
|
|
23
|
+
let braceDepth = 0;
|
|
24
|
+
for (let j = i; j < Math.min(i + 25, lines.length); j++) {
|
|
25
|
+
if (/set[A-Z]\w*\(/.test(lines[j])) hasSetState = true;
|
|
26
|
+
braceDepth += (lines[j].match(/\{/g) || []).length - (lines[j].match(/\}/g) || []).length;
|
|
27
|
+
if (braceDepth <= 0 || /\}\s*,\s*\[/.test(lines[j]) || /^\s*\]\s*\)\s*;?\s*$/.test(lines[j])) {
|
|
28
|
+
if (/\]\s*\)/.test(lines[j])) hasDepArray = true;
|
|
29
|
+
if (braceDepth <= 0) {
|
|
30
|
+
if (j + 1 < lines.length && /^\s*\]\s*\)/.test(lines[j + 1])) hasDepArray = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (hasSetState && !hasDepArray) {
|
|
36
|
+
findings.push({
|
|
37
|
+
ruleId: 'BUG-REACT-001', category: 'bugs', severity: 'high',
|
|
38
|
+
title: 'setState inside useEffect without dependency array — infinite loop',
|
|
39
|
+
description: 'useEffect with setState but no dependency array will re-render infinitely. Add a dependency array.',
|
|
40
|
+
file: fp, line: i + 1, fix: null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'BUG-REACT-002',
|
|
51
|
+
category: 'bugs',
|
|
52
|
+
severity: 'high',
|
|
53
|
+
confidence: 'definite',
|
|
54
|
+
title: 'Direct state mutation instead of setState',
|
|
55
|
+
check({ files }) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const [fp, content] of files) {
|
|
58
|
+
if (!isJSX(fp)) continue;
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
// First find useState declarations to know state variable names
|
|
61
|
+
const stateVars = new Set();
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const m = line.match(/const\s+\[(\w+)\s*,\s*set\w+\]\s*=\s*useState/);
|
|
64
|
+
if (m) stateVars.add(m[1]);
|
|
65
|
+
}
|
|
66
|
+
if (stateVars.size === 0) continue;
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
const line = lines[i];
|
|
69
|
+
for (const sv of stateVars) {
|
|
70
|
+
// Direct mutation patterns: state.push(), state[x] = y, state.splice(), state.sort()
|
|
71
|
+
const mutateRe = new RegExp(`\\b${sv}\\s*\\.\\s*(push|pop|shift|unshift|splice|sort|reverse|fill)\\s*\\(`);
|
|
72
|
+
const assignRe = new RegExp(`\\b${sv}\\s*\\[.+\\]\\s*=`);
|
|
73
|
+
const propAssignRe = new RegExp(`\\b${sv}\\s*\\.\\s*\\w+\\s*=`);
|
|
74
|
+
if (mutateRe.test(line) || assignRe.test(line) || propAssignRe.test(line)) {
|
|
75
|
+
findings.push({
|
|
76
|
+
ruleId: 'BUG-REACT-002', category: 'bugs', severity: 'high',
|
|
77
|
+
title: `Direct mutation of state variable "${sv}" — use set${sv.charAt(0).toUpperCase() + sv.slice(1)}() instead`,
|
|
78
|
+
description: 'Mutating state directly bypasses React\'s reactivity. Use the setter function with a new copy.',
|
|
79
|
+
file: fp, line: i + 1, fix: null,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'BUG-REACT-003',
|
|
91
|
+
category: 'bugs',
|
|
92
|
+
severity: 'high',
|
|
93
|
+
confidence: 'likely',
|
|
94
|
+
title: 'async useEffect callback',
|
|
95
|
+
check({ files }) {
|
|
96
|
+
const findings = [];
|
|
97
|
+
for (const [fp, content] of files) {
|
|
98
|
+
if (!isJSX(fp)) continue;
|
|
99
|
+
const lines = content.split('\n');
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
if (/useEffect\(\s*async/.test(lines[i])) {
|
|
102
|
+
findings.push({
|
|
103
|
+
ruleId: 'BUG-REACT-003', category: 'bugs', severity: 'high',
|
|
104
|
+
title: 'async function passed directly to useEffect',
|
|
105
|
+
description: 'useEffect callbacks cannot be async (React expects a cleanup function or undefined). Wrap in an inner async function.',
|
|
106
|
+
file: fp, line: i + 1, fix: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return findings;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'BUG-REACT-004',
|
|
116
|
+
category: 'bugs',
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
confidence: 'likely',
|
|
119
|
+
title: 'Missing key prop in list rendering',
|
|
120
|
+
check({ files }) {
|
|
121
|
+
const findings = [];
|
|
122
|
+
for (const [fp, content] of files) {
|
|
123
|
+
if (!isJSX(fp)) continue;
|
|
124
|
+
const lines = content.split('\n');
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
// .map(() => <Component ... without key=
|
|
127
|
+
if (/\.map\s*\(/.test(lines[i])) {
|
|
128
|
+
let jsxBlock = '';
|
|
129
|
+
for (let j = i; j < Math.min(i + 8, lines.length); j++) {
|
|
130
|
+
jsxBlock += lines[j];
|
|
131
|
+
}
|
|
132
|
+
// Has JSX return but no key prop
|
|
133
|
+
if (/<\w+/.test(jsxBlock) && !jsxBlock.includes('key=') && !jsxBlock.includes('key ={')) {
|
|
134
|
+
findings.push({
|
|
135
|
+
ruleId: 'BUG-REACT-004', category: 'bugs', severity: 'medium',
|
|
136
|
+
title: 'List rendering with .map() missing key prop',
|
|
137
|
+
description: 'React needs a unique key prop on list items for efficient reconciliation. Using index as key is also an anti-pattern for dynamic lists.',
|
|
138
|
+
file: fp, line: i + 1, fix: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'BUG-REACT-005',
|
|
149
|
+
category: 'bugs',
|
|
150
|
+
severity: 'medium',
|
|
151
|
+
confidence: 'likely',
|
|
152
|
+
title: 'Using array index as key in dynamic list',
|
|
153
|
+
check({ files }) {
|
|
154
|
+
const findings = [];
|
|
155
|
+
for (const [fp, content] of files) {
|
|
156
|
+
if (!isJSX(fp)) continue;
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
159
|
+
// .map((item, index) ... key={index} or key={i}
|
|
160
|
+
if (/\.map\s*\(\s*\(\s*\w+\s*,\s*(\w+)\s*\)/.test(lines[i])) {
|
|
161
|
+
const indexVar = lines[i].match(/\.map\s*\(\s*\(\s*\w+\s*,\s*(\w+)\s*\)/)[1];
|
|
162
|
+
let block = '';
|
|
163
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
164
|
+
block += lines[j] + '\n';
|
|
165
|
+
}
|
|
166
|
+
const keyRe = new RegExp(`key\\s*=\\s*\\{\\s*${indexVar}\\s*\\}`);
|
|
167
|
+
if (keyRe.test(block)) {
|
|
168
|
+
findings.push({
|
|
169
|
+
ruleId: 'BUG-REACT-005', category: 'bugs', severity: 'medium',
|
|
170
|
+
title: 'Array index used as key — causes bugs with reordering, insertion, deletion',
|
|
171
|
+
description: 'Using array index as key causes React to misidentify elements when the list changes. Use a stable unique ID instead.',
|
|
172
|
+
file: fp, line: i + 1, fix: null,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return findings;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'BUG-REACT-006',
|
|
183
|
+
category: 'bugs',
|
|
184
|
+
severity: 'high',
|
|
185
|
+
confidence: 'likely',
|
|
186
|
+
title: 'useEffect cleanup not returned',
|
|
187
|
+
check({ files }) {
|
|
188
|
+
const findings = [];
|
|
189
|
+
for (const [fp, content] of files) {
|
|
190
|
+
if (!isJSX(fp)) continue;
|
|
191
|
+
const lines = content.split('\n');
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
if (/useEffect\(\s*\(\)\s*=>/.test(lines[i])) {
|
|
194
|
+
let block = '';
|
|
195
|
+
let braceDepth = 0;
|
|
196
|
+
let endLine = i;
|
|
197
|
+
for (let j = i; j < Math.min(i + 30, lines.length); j++) {
|
|
198
|
+
block += lines[j] + '\n';
|
|
199
|
+
braceDepth += (lines[j].match(/\{/g) || []).length - (lines[j].match(/\}/g) || []).length;
|
|
200
|
+
if (braceDepth <= 0 && j > i) { endLine = j; break; }
|
|
201
|
+
}
|
|
202
|
+
// Has event listener or interval/timeout but no cleanup return
|
|
203
|
+
const hasSubscription = /addEventListener|setInterval|setTimeout|subscribe|on\(/.test(block);
|
|
204
|
+
const hasCleanup = /return\s*\(\)\s*=>|return\s+function/.test(block);
|
|
205
|
+
if (hasSubscription && !hasCleanup) {
|
|
206
|
+
findings.push({
|
|
207
|
+
ruleId: 'BUG-REACT-006', category: 'bugs', severity: 'high',
|
|
208
|
+
title: 'useEffect with subscription/listener but no cleanup function',
|
|
209
|
+
description: 'Event listeners, intervals, and subscriptions in useEffect must be cleaned up by returning a cleanup function. Otherwise they leak on unmount.',
|
|
210
|
+
file: fp, line: i + 1, fix: null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return findings;
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'BUG-REACT-007',
|
|
221
|
+
category: 'bugs',
|
|
222
|
+
severity: 'medium',
|
|
223
|
+
confidence: 'likely',
|
|
224
|
+
title: 'Stale closure in useCallback/useMemo with missing dependencies',
|
|
225
|
+
check({ files }) {
|
|
226
|
+
const findings = [];
|
|
227
|
+
for (const [fp, content] of files) {
|
|
228
|
+
if (!isJSX(fp)) continue;
|
|
229
|
+
const lines = content.split('\n');
|
|
230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
231
|
+
// useCallback or useMemo with empty dep array
|
|
232
|
+
if (/use(Callback|Memo)\s*\(/.test(lines[i])) {
|
|
233
|
+
let block = '';
|
|
234
|
+
for (let j = i; j < Math.min(i + 15, lines.length); j++) {
|
|
235
|
+
block += lines[j] + '\n';
|
|
236
|
+
if (/\]\s*\)\s*;?\s*$/.test(lines[j])) break;
|
|
237
|
+
}
|
|
238
|
+
// Empty dependency array but references state variables (set* pattern)
|
|
239
|
+
if (/,\s*\[\s*\]\s*\)/.test(block)) {
|
|
240
|
+
// Check if the callback body references any outer variables (heuristic)
|
|
241
|
+
const bodyMatch = block.match(/\(\s*\)\s*=>\s*\{([\s\S]*)\}\s*,/);
|
|
242
|
+
if (bodyMatch) {
|
|
243
|
+
const body = bodyMatch[1];
|
|
244
|
+
// References state vars or props
|
|
245
|
+
if (/\b(props|state|data|items|user|count|value|loading|error|result)\b/.test(body)) {
|
|
246
|
+
findings.push({
|
|
247
|
+
ruleId: 'BUG-REACT-007', category: 'bugs', severity: 'medium',
|
|
248
|
+
title: 'useCallback/useMemo with empty deps may have stale closure',
|
|
249
|
+
description: 'Empty dependency array means the memoized value/callback never updates. If it reads reactive values, they\'ll be stale.',
|
|
250
|
+
file: fp, line: i + 1, fix: null,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return findings;
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'BUG-REACT-008',
|
|
263
|
+
category: 'bugs',
|
|
264
|
+
severity: 'high',
|
|
265
|
+
confidence: 'definite',
|
|
266
|
+
title: 'Conditional hook call',
|
|
267
|
+
check({ files }) {
|
|
268
|
+
const findings = [];
|
|
269
|
+
for (const [fp, content] of files) {
|
|
270
|
+
if (!isJSX(fp)) continue;
|
|
271
|
+
const lines = content.split('\n');
|
|
272
|
+
let inCondition = 0;
|
|
273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
274
|
+
const line = lines[i];
|
|
275
|
+
if (/^\s*if\s*\(/.test(line) || /^\s*else\b/.test(line)) {
|
|
276
|
+
inCondition = 3; // track for next few lines
|
|
277
|
+
}
|
|
278
|
+
if (inCondition > 0) {
|
|
279
|
+
inCondition--;
|
|
280
|
+
if (/\buse(State|Effect|Memo|Callback|Ref|Context|Reducer|LayoutEffect|ImperativeHandle)\s*\(/.test(line)) {
|
|
281
|
+
findings.push({
|
|
282
|
+
ruleId: 'BUG-REACT-008', category: 'bugs', severity: 'high',
|
|
283
|
+
title: 'React hook called conditionally — violates Rules of Hooks',
|
|
284
|
+
description: 'Hooks must be called in the same order on every render. Conditional hooks cause crashes and bugs.',
|
|
285
|
+
file: fp, line: i + 1, fix: null,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return findings;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'BUG-REACT-009',
|
|
296
|
+
category: 'bugs',
|
|
297
|
+
severity: 'medium',
|
|
298
|
+
confidence: 'likely',
|
|
299
|
+
title: 'Object/array literal as default prop or in dependency array',
|
|
300
|
+
check({ files }) {
|
|
301
|
+
const findings = [];
|
|
302
|
+
for (const [fp, content] of files) {
|
|
303
|
+
if (!isJSX(fp)) continue;
|
|
304
|
+
const lines = content.split('\n');
|
|
305
|
+
for (let i = 0; i < lines.length; i++) {
|
|
306
|
+
const line = lines[i];
|
|
307
|
+
// Inline object/array in useEffect/useMemo/useCallback dep array
|
|
308
|
+
if (/use(Effect|Memo|Callback)\s*\(/.test(lines[Math.max(0, i - 10)]?.concat(lines[i]) || '')) {
|
|
309
|
+
// Check for object/array literals in dependency arrays: , [{}, [], {key: val}])
|
|
310
|
+
if (/\[\s*(?:\{[^}]*\}|\[[^\]]*\])\s*\]\s*\)/.test(line)) {
|
|
311
|
+
findings.push({
|
|
312
|
+
ruleId: 'BUG-REACT-009', category: 'bugs', severity: 'medium',
|
|
313
|
+
title: 'Object/array literal in dependency array — triggers on every render',
|
|
314
|
+
description: 'Inline objects/arrays create new references each render, making the dependency array useless. Extract to useMemo or a constant.',
|
|
315
|
+
file: fp, line: i + 1, fix: null,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Default prop as object literal: function Component({ data = {} })
|
|
320
|
+
if (/function\s+[A-Z]\w*\s*\(\s*\{[^}]*=\s*(\[\s*\]|\{\s*\})/.test(line)) {
|
|
321
|
+
findings.push({
|
|
322
|
+
ruleId: 'BUG-REACT-009', category: 'bugs', severity: 'medium',
|
|
323
|
+
title: 'Inline default prop creates new reference each render',
|
|
324
|
+
description: 'Default values like `= {}` or `= []` in destructured props create new references on every render, breaking memoization.',
|
|
325
|
+
file: fp, line: i + 1, fix: null,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return findings;
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'BUG-REACT-010',
|
|
335
|
+
category: 'bugs',
|
|
336
|
+
severity: 'medium',
|
|
337
|
+
confidence: 'likely',
|
|
338
|
+
title: 'setState with stale state reference',
|
|
339
|
+
check({ files }) {
|
|
340
|
+
const findings = [];
|
|
341
|
+
for (const [fp, content] of files) {
|
|
342
|
+
if (!isJSX(fp)) continue;
|
|
343
|
+
const lines = content.split('\n');
|
|
344
|
+
const stateSetters = new Map();
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
const m = line.match(/const\s+\[(\w+)\s*,\s*(set\w+)\]\s*=\s*useState/);
|
|
347
|
+
if (m) stateSetters.set(m[2], m[1]);
|
|
348
|
+
}
|
|
349
|
+
for (let i = 0; i < lines.length; i++) {
|
|
350
|
+
for (const [setter, stateVar] of stateSetters) {
|
|
351
|
+
// setCount(count + 1) instead of setCount(prev => prev + 1)
|
|
352
|
+
const staleRe = new RegExp(`${setter}\\s*\\(\\s*${stateVar}\\s*[+\\-*/]`);
|
|
353
|
+
if (staleRe.test(lines[i])) {
|
|
354
|
+
// Check it's not already using functional form
|
|
355
|
+
const funcRe = new RegExp(`${setter}\\s*\\(\\s*\\w+\\s*=>`);
|
|
356
|
+
if (!funcRe.test(lines[i])) {
|
|
357
|
+
findings.push({
|
|
358
|
+
ruleId: 'BUG-REACT-010', category: 'bugs', severity: 'medium',
|
|
359
|
+
title: `${setter}(${stateVar} + ...) uses stale state — use functional update`,
|
|
360
|
+
description: `Use ${setter}(prev => prev + ...) to avoid stale state bugs, especially in event handlers and async code.`,
|
|
361
|
+
file: fp, line: i + 1, fix: null,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return findings;
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
export default rules;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Bug detection: Regex patterns — ReDoS, common mistakes
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
function isPy(f) { return f.endsWith('.py'); }
|
|
5
|
+
function isSource(f) { return isJS(f) || isPy(f) || f.endsWith('.go') || f.endsWith('.rb'); }
|
|
6
|
+
|
|
7
|
+
const rules = [
|
|
8
|
+
{
|
|
9
|
+
id: 'BUG-REGEX-001',
|
|
10
|
+
category: 'bugs',
|
|
11
|
+
severity: 'high',
|
|
12
|
+
confidence: 'likely',
|
|
13
|
+
title: 'ReDoS — regex with catastrophic backtracking potential',
|
|
14
|
+
check({ files }) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
// Patterns that cause catastrophic backtracking: nested quantifiers, overlapping alternation
|
|
17
|
+
const redosPatterns = [
|
|
18
|
+
/\([^)]*[+*][^)]*\)[+*]/, // (a+)+ or (a*)*
|
|
19
|
+
/\([^)]*\|[^)]*\)[+*]/, // (a|a)+ overlapping alternation with quantifier
|
|
20
|
+
/\.\*[^?].*\.\*/, // .* ... .* without lazy
|
|
21
|
+
/\([^)]+\+\)\{/, // (a+){n,m}
|
|
22
|
+
];
|
|
23
|
+
for (const [fp, content] of files) {
|
|
24
|
+
if (!isSource(fp)) continue;
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
// Find regex literals and new RegExp()
|
|
29
|
+
const regexMatches = [
|
|
30
|
+
...line.matchAll(/\/([^/\n]{5,})\/[gimsuvy]*/g),
|
|
31
|
+
...line.matchAll(/new\s+RegExp\s*\(\s*['"`]([^'"`]{5,})['"`]/g),
|
|
32
|
+
];
|
|
33
|
+
for (const m of regexMatches) {
|
|
34
|
+
const pattern = m[1];
|
|
35
|
+
for (const redos of redosPatterns) {
|
|
36
|
+
if (redos.test(pattern)) {
|
|
37
|
+
findings.push({
|
|
38
|
+
ruleId: 'BUG-REGEX-001', category: 'bugs', severity: 'high',
|
|
39
|
+
title: 'Regex with catastrophic backtracking (ReDoS) potential',
|
|
40
|
+
description: 'This regex has nested quantifiers or overlapping patterns that can cause exponential backtracking. An attacker can craft input that hangs the process. Use atomic groups or rewrite the pattern.',
|
|
41
|
+
file: fp, line: i + 1, fix: null,
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return findings;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'BUG-REGEX-002',
|
|
54
|
+
category: 'bugs',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
confidence: 'definite',
|
|
57
|
+
title: 'User input passed to new RegExp() — regex injection',
|
|
58
|
+
check({ files }) {
|
|
59
|
+
const findings = [];
|
|
60
|
+
for (const [fp, content] of files) {
|
|
61
|
+
if (!isJS(fp)) continue;
|
|
62
|
+
const lines = content.split('\n');
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
// new RegExp(variable) or new RegExp(req.something)
|
|
65
|
+
if (/new\s+RegExp\s*\(\s*(?:req\.|params|query|input|user|body|search|filter|pattern|term|keyword)\w*/i.test(lines[i])) {
|
|
66
|
+
// Check if escaped
|
|
67
|
+
const context = lines.slice(Math.max(0, i - 3), i + 1).join('\n');
|
|
68
|
+
if (!/escapeRegex|escapeRegExp|escape.*[Rr]eg|sanitize|lodash.*escape|_.escape/.test(context)) {
|
|
69
|
+
findings.push({
|
|
70
|
+
ruleId: 'BUG-REGEX-002', category: 'bugs', severity: 'high',
|
|
71
|
+
title: 'User input in new RegExp() — regex injection and ReDoS risk',
|
|
72
|
+
description: 'User input must be escaped before passing to RegExp. Use a helper: str.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")',
|
|
73
|
+
file: fp, line: i + 1, fix: null,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'BUG-REGEX-003',
|
|
84
|
+
category: 'bugs',
|
|
85
|
+
severity: 'medium',
|
|
86
|
+
confidence: 'likely',
|
|
87
|
+
title: 'Email regex too simple — rejects valid addresses',
|
|
88
|
+
check({ files }) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const [fp, content] of files) {
|
|
91
|
+
if (!isSource(fp)) continue;
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
// Common too-simple email regex patterns
|
|
95
|
+
if (/email|mail/i.test(lines[i]) && /\/\^?\[?\\?[ws@.\-+]+\$?\//i.test(lines[i])) {
|
|
96
|
+
// Check if it's a validation regex that's too simple
|
|
97
|
+
if (/\.\+@\.\+\.\.\+/.test(lines[i]) || /\\w\+@\\w\+/.test(lines[i]) || /\[a-z\].*@.*\[a-z\]/.test(lines[i])) {
|
|
98
|
+
findings.push({
|
|
99
|
+
ruleId: 'BUG-REGEX-003', category: 'bugs', severity: 'medium',
|
|
100
|
+
title: 'Email validation regex too simple — rejects valid emails',
|
|
101
|
+
description: 'Simple email regexes reject valid addresses (plus-addressing, dots, international domains). Use a library or the HTML5 email input type.',
|
|
102
|
+
file: fp, line: i + 1, fix: null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'BUG-REGEX-004',
|
|
113
|
+
category: 'bugs',
|
|
114
|
+
severity: 'medium',
|
|
115
|
+
confidence: 'definite',
|
|
116
|
+
title: 'Regex special char not escaped in new RegExp(string)',
|
|
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
|
+
// new RegExp("string.with.dots") — dots should be \.
|
|
124
|
+
const m = lines[i].match(/new\s+RegExp\s*\(\s*(['"`])([^'"`]+)\1\s*\)/);
|
|
125
|
+
if (m) {
|
|
126
|
+
const pattern = m[2];
|
|
127
|
+
// Has unescaped dots that look like literal dots (not \.)
|
|
128
|
+
if (/[^\\]\.[^*+?{(|]/.test(pattern) && !/\[.*\..*\]/.test(pattern)) {
|
|
129
|
+
if (/\.\w+\b/.test(pattern)) { // looks like literal string matching
|
|
130
|
+
findings.push({
|
|
131
|
+
ruleId: 'BUG-REGEX-004', category: 'bugs', severity: 'medium',
|
|
132
|
+
title: 'Unescaped dot in RegExp string — matches any character, not literal "."',
|
|
133
|
+
description: 'In regex, "." matches any character. To match a literal dot, use "\\\\." in the string. In a RegExp string, you need double backslash.',
|
|
134
|
+
file: fp, line: i + 1, fix: null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'BUG-REGEX-005',
|
|
146
|
+
category: 'bugs',
|
|
147
|
+
severity: 'medium',
|
|
148
|
+
confidence: 'likely',
|
|
149
|
+
title: 'Regex used for URL/path validation without anchors',
|
|
150
|
+
check({ files }) {
|
|
151
|
+
const findings = [];
|
|
152
|
+
for (const [fp, content] of files) {
|
|
153
|
+
if (!isJS(fp)) continue;
|
|
154
|
+
const lines = content.split('\n');
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
// .test() or .match() with a URL/path pattern without ^ and $
|
|
157
|
+
if (/\.(test|match)\s*\(\s*\/(?!.*\^)(?!.*\$).*(?:https?|www\.|\.com|api\/|\/\w+\/)/i.test(lines[i])) {
|
|
158
|
+
if (/valid|check|is[A-Z]|allow|block|filter/.test(lines[i]) || /valid|check|is[A-Z]|allow/.test(lines[Math.max(0, i - 2)])) {
|
|
159
|
+
findings.push({
|
|
160
|
+
ruleId: 'BUG-REGEX-005', category: 'bugs', severity: 'medium',
|
|
161
|
+
title: 'URL/path regex without anchors (^ $) — partial matches bypass validation',
|
|
162
|
+
description: 'Without ^ and $, the regex matches substrings. "https://evil.com/https://safe.com" would pass a check for safe.com. Add ^ and $ anchors.',
|
|
163
|
+
file: fp, line: i + 1, fix: null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'BUG-REGEX-006',
|
|
174
|
+
category: 'bugs',
|
|
175
|
+
severity: 'medium',
|
|
176
|
+
confidence: 'definite',
|
|
177
|
+
title: 'replaceAll with regex missing /g flag',
|
|
178
|
+
check({ files }) {
|
|
179
|
+
const findings = [];
|
|
180
|
+
for (const [fp, content] of files) {
|
|
181
|
+
if (!isJS(fp)) continue;
|
|
182
|
+
const lines = content.split('\n');
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
// .replaceAll(/pattern/) without g flag — throws TypeError
|
|
185
|
+
if (/\.replaceAll\s*\(\s*\/[^/]+\/(?![gimsuvy]*g)/.test(lines[i])) {
|
|
186
|
+
findings.push({
|
|
187
|
+
ruleId: 'BUG-REGEX-006', category: 'bugs', severity: 'medium',
|
|
188
|
+
title: '.replaceAll() with regex missing /g flag — throws TypeError',
|
|
189
|
+
description: 'String.replaceAll() requires the /g flag when used with a regex. Without it, JavaScript throws a TypeError at runtime.',
|
|
190
|
+
file: fp, line: i + 1, fix: null,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return findings;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
export default rules;
|