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,73 @@
|
|
|
1
|
+
import asyncRules from './js-async.js';
|
|
2
|
+
import typeCoercionRules from './js-type-coercion.js';
|
|
3
|
+
import arrayObjectRules from './js-array-object.js';
|
|
4
|
+
import logicRules from './js-logic.js';
|
|
5
|
+
import closureScopeRules from './js-closure-scope.js';
|
|
6
|
+
import errorHandlingRules from './js-error-handling.js';
|
|
7
|
+
import fixableRules from './js-async-fixable.js';
|
|
8
|
+
import pythonRules from './python.js';
|
|
9
|
+
import pythonFixableRules from './python-fixable.js';
|
|
10
|
+
import generalRules from './general.js';
|
|
11
|
+
import reactRules from './js-react.js';
|
|
12
|
+
import nodeRules from './js-node.js';
|
|
13
|
+
import aiCodegenRules from './ai-codegen.js';
|
|
14
|
+
import apiRules from './js-api.js';
|
|
15
|
+
import tsRules from './ts-bugs.js';
|
|
16
|
+
import stateRules from './js-state.js';
|
|
17
|
+
import pythonAdvancedRules from './python-advanced.js';
|
|
18
|
+
import goRules from './go-bugs.js';
|
|
19
|
+
import rubyRules from './ruby-bugs.js';
|
|
20
|
+
import shellRules from './shell-bugs.js';
|
|
21
|
+
import reactFixableRules from './react-fixable.js';
|
|
22
|
+
import nodeFixableRules from './node-fixable.js';
|
|
23
|
+
import aiCodegenFixableRules from './ai-codegen-fixable.js';
|
|
24
|
+
import nextjsRules from './nextjs-bugs.js';
|
|
25
|
+
import regexRules from './js-regex.js';
|
|
26
|
+
import memoryRules from './js-memory.js';
|
|
27
|
+
import a11yRules from './accessibility.js';
|
|
28
|
+
import dockerRules from './docker-bugs.js';
|
|
29
|
+
import databaseRules from './js-database.js';
|
|
30
|
+
import nextjsFixableRules from './nextjs-fixable.js';
|
|
31
|
+
import silentFailureRules from './silent-failures.js';
|
|
32
|
+
import cryptoBugRules from './crypto-bugs.js';
|
|
33
|
+
import codeSmellRules from './code-smell-bugs.js';
|
|
34
|
+
import unusedVarsRules from './unused-vars.js';
|
|
35
|
+
|
|
36
|
+
const allBugRules = [
|
|
37
|
+
...asyncRules,
|
|
38
|
+
...typeCoercionRules,
|
|
39
|
+
...arrayObjectRules,
|
|
40
|
+
...logicRules,
|
|
41
|
+
...closureScopeRules,
|
|
42
|
+
...errorHandlingRules,
|
|
43
|
+
...fixableRules,
|
|
44
|
+
...pythonRules,
|
|
45
|
+
...pythonFixableRules,
|
|
46
|
+
...generalRules,
|
|
47
|
+
...reactRules,
|
|
48
|
+
...nodeRules,
|
|
49
|
+
...aiCodegenRules,
|
|
50
|
+
...apiRules,
|
|
51
|
+
...tsRules,
|
|
52
|
+
...stateRules,
|
|
53
|
+
...pythonAdvancedRules,
|
|
54
|
+
...goRules,
|
|
55
|
+
...rubyRules,
|
|
56
|
+
...shellRules,
|
|
57
|
+
...reactFixableRules,
|
|
58
|
+
...nodeFixableRules,
|
|
59
|
+
...aiCodegenFixableRules,
|
|
60
|
+
...nextjsRules,
|
|
61
|
+
...regexRules,
|
|
62
|
+
...memoryRules,
|
|
63
|
+
...a11yRules,
|
|
64
|
+
...dockerRules,
|
|
65
|
+
...databaseRules,
|
|
66
|
+
...nextjsFixableRules,
|
|
67
|
+
...silentFailureRules,
|
|
68
|
+
...cryptoBugRules,
|
|
69
|
+
...codeSmellRules,
|
|
70
|
+
...unusedVarsRules,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export default allBugRules;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Bug detection: API call and data fetching 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-API-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'definite',
|
|
11
|
+
title: 'fetch() without checking response.ok',
|
|
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 (/\bfetch\s*\(/.test(lines[i]) && !/import|require|mock|test|spec/.test(lines[i])) {
|
|
19
|
+
let block = '';
|
|
20
|
+
for (let j = i; j < Math.min(i + 12, lines.length); j++) {
|
|
21
|
+
block += lines[j] + '\n';
|
|
22
|
+
}
|
|
23
|
+
if (/\.json\s*\(\)/.test(block) && !/response\.ok|res\.ok|\.ok\b|\.status\s*[!=]==?\s*200|if\s*\(\s*!/.test(block)) {
|
|
24
|
+
findings.push({
|
|
25
|
+
ruleId: 'BUG-API-001', category: 'bugs', severity: 'high',
|
|
26
|
+
title: 'fetch() calls .json() without checking response.ok',
|
|
27
|
+
description: 'fetch() does not throw on HTTP errors (404, 500). Always check response.ok before parsing. A 500 response will still try to parse as JSON and give cryptic errors.',
|
|
28
|
+
file: fp, line: i + 1, fix: null,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'BUG-API-002',
|
|
39
|
+
category: 'bugs',
|
|
40
|
+
severity: 'medium',
|
|
41
|
+
confidence: 'likely',
|
|
42
|
+
title: 'API call without timeout',
|
|
43
|
+
check({ files }) {
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const [fp, content] of files) {
|
|
46
|
+
if (!isJS(fp)) continue;
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
if (/\bfetch\s*\(\s*['"`\w]/.test(lines[i])) {
|
|
50
|
+
let block = '';
|
|
51
|
+
for (let j = i; j < Math.min(i + 6, lines.length); j++) {
|
|
52
|
+
block += lines[j];
|
|
53
|
+
}
|
|
54
|
+
if (!/signal|timeout|AbortController|setTimeout/.test(block)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
ruleId: 'BUG-API-002', category: 'bugs', severity: 'medium',
|
|
57
|
+
title: 'fetch() without timeout — request can hang forever',
|
|
58
|
+
description: 'fetch() has no default timeout. Use AbortController with setTimeout to prevent hanging requests.',
|
|
59
|
+
file: fp, line: i + 1, fix: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'BUG-API-003',
|
|
70
|
+
category: 'bugs',
|
|
71
|
+
severity: 'medium',
|
|
72
|
+
confidence: 'likely',
|
|
73
|
+
title: 'No retry logic for network requests',
|
|
74
|
+
check({ files }) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (const [fp, content] of files) {
|
|
77
|
+
if (!isJS(fp)) continue;
|
|
78
|
+
// Only flag if file has multiple fetch/axios calls (it's an API-heavy file)
|
|
79
|
+
const fetchCount = (content.match(/\bfetch\s*\(|\baxios\.\w+\s*\(/g) || []).length;
|
|
80
|
+
if (fetchCount < 3) continue;
|
|
81
|
+
if (!/retry|retries|attempts|backoff|MAX_RETRIES/i.test(content)) {
|
|
82
|
+
findings.push({
|
|
83
|
+
ruleId: 'BUG-API-003', category: 'bugs', severity: 'medium',
|
|
84
|
+
title: `${fetchCount} API calls without retry logic — failures are permanent`,
|
|
85
|
+
description: 'Network requests fail transiently. API-heavy code should implement retry with exponential backoff for resilience.',
|
|
86
|
+
file: fp, line: 1, fix: null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'BUG-API-004',
|
|
95
|
+
category: 'bugs',
|
|
96
|
+
severity: 'high',
|
|
97
|
+
confidence: 'likely',
|
|
98
|
+
title: 'Secrets in fetch/axios URL or headers',
|
|
99
|
+
check({ files }) {
|
|
100
|
+
const findings = [];
|
|
101
|
+
for (const [fp, content] of files) {
|
|
102
|
+
if (!isJS(fp)) continue;
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
// API key in URL query param
|
|
106
|
+
if (/\bfetch\s*\(.*[?&](api_?key|token|secret|password|key)=/i.test(lines[i])) {
|
|
107
|
+
findings.push({
|
|
108
|
+
ruleId: 'BUG-API-004', category: 'bugs', severity: 'high',
|
|
109
|
+
title: 'API key/secret in URL query parameter — visible in logs and browser history',
|
|
110
|
+
description: 'Secrets in URLs are logged by servers, proxies, and browsers. Pass them in headers (Authorization) instead.',
|
|
111
|
+
file: fp, line: i + 1, fix: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Hardcoded bearer token
|
|
115
|
+
if (/['"`]Bearer\s+[a-zA-Z0-9_\-.]{20,}['"`]/.test(lines[i]) && !/process\.env|import\.meta\.env|\$\{/.test(lines[i])) {
|
|
116
|
+
findings.push({
|
|
117
|
+
ruleId: 'BUG-API-004', category: 'bugs', severity: 'high',
|
|
118
|
+
title: 'Hardcoded Bearer token in source code',
|
|
119
|
+
description: 'Bearer tokens should come from environment variables or secure config, not hardcoded in source.',
|
|
120
|
+
file: fp, line: i + 1, fix: null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return findings;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'BUG-API-005',
|
|
130
|
+
category: 'bugs',
|
|
131
|
+
severity: 'medium',
|
|
132
|
+
confidence: 'likely',
|
|
133
|
+
title: 'Race condition in concurrent API calls',
|
|
134
|
+
check({ files }) {
|
|
135
|
+
const findings = [];
|
|
136
|
+
for (const [fp, content] of files) {
|
|
137
|
+
if (!isJS(fp)) continue;
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
// Multiple independent awaits that could be parallel
|
|
141
|
+
if (/^\s*(?:const|let|var)\s+\w+\s*=\s*await\s/.test(lines[i])) {
|
|
142
|
+
let consecutiveAwaits = 1;
|
|
143
|
+
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
144
|
+
if (/^\s*(?:const|let|var)\s+\w+\s*=\s*await\s/.test(lines[j])) {
|
|
145
|
+
consecutiveAwaits++;
|
|
146
|
+
} else if (lines[j].trim() && !/^\s*\/\//.test(lines[j])) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (consecutiveAwaits >= 3) {
|
|
151
|
+
findings.push({
|
|
152
|
+
ruleId: 'BUG-API-005', category: 'bugs', severity: 'medium',
|
|
153
|
+
title: `${consecutiveAwaits} sequential awaits that could run in parallel with Promise.all()`,
|
|
154
|
+
description: 'Sequential awaits for independent operations waste time. Use Promise.all([...]) to run them concurrently.',
|
|
155
|
+
file: fp, line: i + 1, fix: null,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return findings;
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'BUG-API-006',
|
|
166
|
+
category: 'bugs',
|
|
167
|
+
severity: 'high',
|
|
168
|
+
confidence: 'likely',
|
|
169
|
+
title: 'Sensitive data in URL path parameters',
|
|
170
|
+
check({ files }) {
|
|
171
|
+
const findings = [];
|
|
172
|
+
for (const [fp, content] of files) {
|
|
173
|
+
if (!isJS(fp)) continue;
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
// fetch(`/api/users/${email}`) or similar with PII in path
|
|
177
|
+
if (/fetch\s*\(\s*`[^`]*\$\{[^}]*(email|ssn|password|creditCard|cardNumber|ssn|phone|socialSecurity)[^}]*\}/.test(lines[i])) {
|
|
178
|
+
findings.push({
|
|
179
|
+
ruleId: 'BUG-API-006', category: 'bugs', severity: 'high',
|
|
180
|
+
title: 'Sensitive data (PII) in URL path — logged by servers and CDNs',
|
|
181
|
+
description: 'URLs with PII are logged everywhere. Send sensitive data in the request body (POST) or encrypted headers.',
|
|
182
|
+
file: fp, line: i + 1, fix: null,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'BUG-API-007',
|
|
192
|
+
category: 'bugs',
|
|
193
|
+
severity: 'medium',
|
|
194
|
+
confidence: 'likely',
|
|
195
|
+
title: 'Large payload without pagination',
|
|
196
|
+
check({ files }) {
|
|
197
|
+
const findings = [];
|
|
198
|
+
for (const [fp, content] of files) {
|
|
199
|
+
if (!isJS(fp)) continue;
|
|
200
|
+
// API route that returns all records
|
|
201
|
+
if (!/express|fastify|router/.test(content)) continue;
|
|
202
|
+
const lines = content.split('\n');
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
if (/\.(get)\s*\(\s*['"`].*(?:\/all|\/list|s['"`])/.test(lines[i])) {
|
|
205
|
+
let block = '';
|
|
206
|
+
for (let j = i; j < Math.min(i + 20, lines.length); j++) {
|
|
207
|
+
block += lines[j] + '\n';
|
|
208
|
+
}
|
|
209
|
+
if (/\.(find|findAll|findMany|select)\s*\(\s*(?:\{\s*\})?\s*\)/.test(block) && !/limit|skip|offset|page|cursor|pagina/i.test(block)) {
|
|
210
|
+
findings.push({
|
|
211
|
+
ruleId: 'BUG-API-007', category: 'bugs', severity: 'medium',
|
|
212
|
+
title: 'API endpoint returns all records without pagination',
|
|
213
|
+
description: 'Returning all records crashes with large datasets. Add pagination (limit/offset or cursor-based).',
|
|
214
|
+
file: fp, line: i + 1, fix: null,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'BUG-API-008',
|
|
225
|
+
category: 'bugs',
|
|
226
|
+
severity: 'high',
|
|
227
|
+
confidence: 'likely',
|
|
228
|
+
title: 'DELETE endpoint without authentication check',
|
|
229
|
+
check({ files }) {
|
|
230
|
+
const findings = [];
|
|
231
|
+
for (const [fp, content] of files) {
|
|
232
|
+
if (!isJS(fp)) continue;
|
|
233
|
+
if (!/express|fastify|router/.test(content)) continue;
|
|
234
|
+
const lines = content.split('\n');
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
if (/\.delete\s*\(\s*['"`]/.test(lines[i])) {
|
|
237
|
+
let block = '';
|
|
238
|
+
for (let j = i; j < Math.min(i + 15, lines.length); j++) {
|
|
239
|
+
block += lines[j] + '\n';
|
|
240
|
+
}
|
|
241
|
+
if (!/auth|authenticate|authorize|isAdmin|isLoggedIn|protect|guard|middleware|jwt|token|session|req\.user/.test(block)) {
|
|
242
|
+
findings.push({
|
|
243
|
+
ruleId: 'BUG-API-008', category: 'bugs', severity: 'high',
|
|
244
|
+
title: 'DELETE endpoint without authentication — anyone can delete resources',
|
|
245
|
+
description: 'AI generators often create CRUD endpoints without auth middleware on destructive operations.',
|
|
246
|
+
file: fp, line: i + 1, fix: null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return findings;
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
export default rules;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Bug detection: JavaScript array/object mutation bugs
|
|
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-ARR-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'Array modified during iteration — splice/push/shift in for loop',
|
|
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
|
+
let inForLoop = 0;
|
|
18
|
+
let loopArrayName = null;
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const forMatch = line.match(/for\s*\(\s*(?:let|var|const)\s+\w+\s*(?:=\s*0|of|in)\s*(?:;|\s)?\s*(?:\w+\.length|(\w+))/);
|
|
22
|
+
if (forMatch) { inForLoop++; loopArrayName = forMatch[1]; }
|
|
23
|
+
if (/\.forEach\s*\(/.test(line) || /\.map\s*\(/.test(line)) { inForLoop++; }
|
|
24
|
+
if (inForLoop > 0) {
|
|
25
|
+
if (/\.(splice|push|pop|shift|unshift)\s*\(/.test(line)) {
|
|
26
|
+
findings.push({
|
|
27
|
+
ruleId: 'BUG-ARR-001', category: 'bugs', severity: 'high',
|
|
28
|
+
title: 'Array modified during iteration — indices shift, elements skipped',
|
|
29
|
+
description: 'Calling splice/push/shift on an array while iterating over it causes elements to be skipped or processed twice. Iterate over a copy or collect changes.',
|
|
30
|
+
file: fp, line: i + 1, fix: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (/\}/.test(line) && inForLoop > 0) inForLoop--;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return findings;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'BUG-ARR-002',
|
|
42
|
+
category: 'bugs',
|
|
43
|
+
severity: 'high',
|
|
44
|
+
confidence: 'likely',
|
|
45
|
+
title: 'delete on array element — leaves a hole, does not re-index',
|
|
46
|
+
check({ files }) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const [fp, content] of files) {
|
|
49
|
+
if (!isJS(fp)) continue;
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
if (/delete\s+\w+\[\d+\]/.test(lines[i]) || /delete\s+\w+\[\w+\]/.test(lines[i])) {
|
|
53
|
+
const context = lines.slice(Math.max(0, i - 3), i + 1).join('\n');
|
|
54
|
+
if (/(?:array|arr|list|items|results|data)\b/i.test(context) || /\[\s*\d/.test(context)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
ruleId: 'BUG-ARR-002', category: 'bugs', severity: 'high',
|
|
57
|
+
title: 'delete on array element — creates sparse array with holes',
|
|
58
|
+
description: 'delete arr[i] sets the element to undefined but keeps the index. Use arr.splice(i, 1) to actually remove it.',
|
|
59
|
+
file: fp, line: i + 1, fix: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'BUG-ARR-003',
|
|
70
|
+
category: 'bugs',
|
|
71
|
+
severity: 'high',
|
|
72
|
+
confidence: 'likely',
|
|
73
|
+
title: 'return inside forEach has no effect on outer function',
|
|
74
|
+
check({ files }) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (const [fp, content] of files) {
|
|
77
|
+
if (!isJS(fp)) continue;
|
|
78
|
+
const lines = content.split('\n');
|
|
79
|
+
let forEachDepth = 0;
|
|
80
|
+
for (let i = 0; i < lines.length; i++) {
|
|
81
|
+
const line = lines[i];
|
|
82
|
+
if (/\.forEach\s*\(/.test(line)) forEachDepth++;
|
|
83
|
+
if (forEachDepth > 0 && /\breturn\b/.test(line) && !/return\s*;/.test(line)) {
|
|
84
|
+
// return with a value inside forEach — likely trying to return from outer function
|
|
85
|
+
findings.push({
|
|
86
|
+
ruleId: 'BUG-ARR-003', category: 'bugs', severity: 'high',
|
|
87
|
+
title: 'return inside forEach — does not return from outer function',
|
|
88
|
+
description: 'return inside forEach only exits the current callback iteration. Use for...of, .find(), .some(), or .filter() instead.',
|
|
89
|
+
file: fp, line: i + 1, fix: null,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (/\}/.test(line) && forEachDepth > 0) forEachDepth--;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'BUG-ARR-004',
|
|
100
|
+
category: 'bugs',
|
|
101
|
+
severity: 'medium',
|
|
102
|
+
confidence: 'likely',
|
|
103
|
+
title: 'Array.includes() with NaN — always returns false for indexOf',
|
|
104
|
+
check({ files }) {
|
|
105
|
+
const findings = [];
|
|
106
|
+
for (const [fp, content] of files) {
|
|
107
|
+
if (!isJS(fp)) continue;
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
if (/\.indexOf\s*\(\s*NaN\s*\)/.test(lines[i])) {
|
|
111
|
+
findings.push({
|
|
112
|
+
ruleId: 'BUG-ARR-004', category: 'bugs', severity: 'medium',
|
|
113
|
+
title: 'indexOf(NaN) always returns -1 — NaN !== NaN',
|
|
114
|
+
description: 'Array.indexOf() uses strict equality. Since NaN !== NaN, indexOf(NaN) never finds it. Use Array.includes(NaN) or Array.findIndex(Number.isNaN).',
|
|
115
|
+
file: fp, line: i + 1, fix: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return findings;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'BUG-ARR-005',
|
|
125
|
+
category: 'bugs',
|
|
126
|
+
severity: 'high',
|
|
127
|
+
confidence: 'likely',
|
|
128
|
+
title: 'Spread in reduce accumulator — O(n^2) copy on each iteration',
|
|
129
|
+
check({ files }) {
|
|
130
|
+
const findings = [];
|
|
131
|
+
for (const [fp, content] of files) {
|
|
132
|
+
if (!isJS(fp)) continue;
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
let inReduce = 0;
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
if (/\.reduce\s*\(/.test(lines[i])) inReduce = 5;
|
|
137
|
+
if (inReduce > 0) {
|
|
138
|
+
if (/\[\s*\.\.\.(?:acc|prev|result|memo)/.test(lines[i]) || /\{\s*\.\.\.(?:acc|prev|result|memo)/.test(lines[i])) {
|
|
139
|
+
findings.push({
|
|
140
|
+
ruleId: 'BUG-ARR-005', category: 'bugs', severity: 'high',
|
|
141
|
+
title: 'Spread in reduce accumulator — creates O(n^2) copies',
|
|
142
|
+
description: 'Spreading the accumulator in reduce copies the entire array/object every iteration. Use push() or direct mutation on the accumulator.',
|
|
143
|
+
file: fp, line: i + 1, fix: null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
inReduce--;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'BUG-ARR-006',
|
|
155
|
+
category: 'bugs',
|
|
156
|
+
severity: 'high',
|
|
157
|
+
confidence: 'likely',
|
|
158
|
+
title: 'Off-by-one: loop uses <= array.length instead of <',
|
|
159
|
+
check({ files }) {
|
|
160
|
+
const findings = [];
|
|
161
|
+
for (const [fp, content] of files) {
|
|
162
|
+
if (!isJS(fp)) continue;
|
|
163
|
+
const lines = content.split('\n');
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
if (/\w+\s*<=\s*\w+\.length\b/.test(lines[i]) && /for\s*\(/.test(lines[i])) {
|
|
166
|
+
findings.push({
|
|
167
|
+
ruleId: 'BUG-ARR-006', category: 'bugs', severity: 'high',
|
|
168
|
+
title: 'Off-by-one: i <= arr.length accesses undefined element',
|
|
169
|
+
description: 'Array indices go from 0 to length-1. Using <= causes an access at arr[arr.length] which is undefined.',
|
|
170
|
+
file: fp, line: i + 1, fix: null,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return findings;
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'BUG-ARR-007',
|
|
180
|
+
category: 'bugs',
|
|
181
|
+
severity: 'medium',
|
|
182
|
+
confidence: 'likely',
|
|
183
|
+
title: 'String.replace() without /g flag — only replaces first occurrence',
|
|
184
|
+
check({ files }) {
|
|
185
|
+
const findings = [];
|
|
186
|
+
for (const [fp, content] of files) {
|
|
187
|
+
if (!isJS(fp)) continue;
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
// .replace("string", ...) or .replace('string', ...)
|
|
192
|
+
if (/\.replace\s*\(\s*['"][^'"]+['"]/.test(line) && !line.includes('replaceAll')) {
|
|
193
|
+
// Check if the context suggests they want all occurrences
|
|
194
|
+
if (/(?:all|every|each|clean|strip|remove|sanitize)/i.test(line)) {
|
|
195
|
+
findings.push({
|
|
196
|
+
ruleId: 'BUG-ARR-007', category: 'bugs', severity: 'medium',
|
|
197
|
+
title: 'String.replace() with string arg — only replaces first match',
|
|
198
|
+
description: '.replace("x", "y") only replaces the first occurrence. Use .replaceAll() or .replace(/x/g, "y") for all matches.',
|
|
199
|
+
file: fp, line: i + 1, fix: null,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return findings;
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
export default rules;
|