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,207 @@
|
|
|
1
|
+
// Auto-fixable React bug patterns
|
|
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-RFIX-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'definite',
|
|
11
|
+
title: 'async useEffect → wrap in inner function',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJSX(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const match = lines[i].match(/^(\s*)useEffect\(\s*async\s*\(\)\s*=>\s*\{/);
|
|
19
|
+
if (match) {
|
|
20
|
+
findings.push({
|
|
21
|
+
ruleId: 'BUG-RFIX-001', category: 'bugs', severity: 'high',
|
|
22
|
+
title: 'async useEffect → wrap in inner IIFE',
|
|
23
|
+
description: 'useEffect cannot be async. Wrap the async logic in an inner function.',
|
|
24
|
+
file: fp, line: i + 1,
|
|
25
|
+
fix: {
|
|
26
|
+
type: 'replace',
|
|
27
|
+
old: 'useEffect(async () => {',
|
|
28
|
+
new: `useEffect(() => {\n${match[1]} const run = async () => {`,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'BUG-RFIX-002',
|
|
39
|
+
category: 'bugs',
|
|
40
|
+
severity: 'medium',
|
|
41
|
+
confidence: 'definite',
|
|
42
|
+
title: 'console.log in JSX component → remove',
|
|
43
|
+
check({ files }) {
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const [fp, content] of files) {
|
|
46
|
+
if (!isJSX(fp)) continue;
|
|
47
|
+
if (!/function\s+[A-Z]|const\s+[A-Z]\w+\s*=/.test(content)) continue; // Only React components
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
if (/^\s*console\.log\s*\(/.test(lines[i]) && /\)\s*;?\s*$/.test(lines[i])) {
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: 'BUG-RFIX-002', category: 'bugs', severity: 'medium',
|
|
53
|
+
title: 'Debug console.log left in component',
|
|
54
|
+
description: 'Remove debug logging from production components.',
|
|
55
|
+
file: fp, line: i + 1,
|
|
56
|
+
fix: {
|
|
57
|
+
type: 'replace',
|
|
58
|
+
old: lines[i],
|
|
59
|
+
new: '',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'BUG-RFIX-003',
|
|
70
|
+
category: 'bugs',
|
|
71
|
+
severity: 'medium',
|
|
72
|
+
confidence: 'definite',
|
|
73
|
+
title: '@ts-ignore → @ts-expect-error',
|
|
74
|
+
check({ files }) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (const [fp, content] of files) {
|
|
77
|
+
if (!fp.endsWith('.ts') && !fp.endsWith('.tsx')) continue;
|
|
78
|
+
const lines = content.split('\n');
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
if (/@ts-ignore/.test(lines[i]) && !/@ts-expect-error/.test(lines[i])) {
|
|
81
|
+
findings.push({
|
|
82
|
+
ruleId: 'BUG-RFIX-003', category: 'bugs', severity: 'medium',
|
|
83
|
+
title: '@ts-ignore → @ts-expect-error (will warn when suppression is unnecessary)',
|
|
84
|
+
file: fp, line: i + 1,
|
|
85
|
+
fix: {
|
|
86
|
+
type: 'replace',
|
|
87
|
+
old: '@ts-ignore',
|
|
88
|
+
new: '@ts-expect-error',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return findings;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'BUG-RFIX-004',
|
|
99
|
+
category: 'bugs',
|
|
100
|
+
severity: 'high',
|
|
101
|
+
confidence: 'definite',
|
|
102
|
+
title: 'return next() pattern fix for Express middleware',
|
|
103
|
+
check({ files }) {
|
|
104
|
+
const findings = [];
|
|
105
|
+
for (const [fp, content] of files) {
|
|
106
|
+
if (!isJSX(fp) && !fp.endsWith('.js') && !fp.endsWith('.mjs')) continue;
|
|
107
|
+
if (!/express|middleware|router/.test(content)) continue;
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
// next() on its own line (not return next())
|
|
111
|
+
if (/^\s+next\s*\(\s*\)\s*;?\s*$/.test(lines[i]) && !/return/.test(lines[i])) {
|
|
112
|
+
// Check if there's code after
|
|
113
|
+
const nextLine = lines[i + 1]?.trim();
|
|
114
|
+
if (nextLine && nextLine !== '}' && nextLine !== '});' && !nextLine.startsWith('//')) {
|
|
115
|
+
findings.push({
|
|
116
|
+
ruleId: 'BUG-RFIX-004', category: 'bugs', severity: 'high',
|
|
117
|
+
title: 'next() without return — add return to prevent continued execution',
|
|
118
|
+
file: fp, line: i + 1,
|
|
119
|
+
fix: {
|
|
120
|
+
type: 'replace',
|
|
121
|
+
old: lines[i],
|
|
122
|
+
new: lines[i].replace(/(\s*)next\s*\(\s*\)/, '$1return next()'),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return findings;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'BUG-RFIX-005',
|
|
134
|
+
category: 'bugs',
|
|
135
|
+
severity: 'high',
|
|
136
|
+
confidence: 'definite',
|
|
137
|
+
title: 'fetch without response.ok check → add check',
|
|
138
|
+
check({ files }) {
|
|
139
|
+
const findings = [];
|
|
140
|
+
for (const [fp, content] of files) {
|
|
141
|
+
if (!isJSX(fp) && !fp.endsWith('.js') && !fp.endsWith('.mjs')) continue;
|
|
142
|
+
const lines = content.split('\n');
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
// const response = await fetch(...)
|
|
145
|
+
const match = lines[i].match(/^(\s*)(?:const|let)\s+(response|res)\s*=\s*await\s+fetch\s*\(/);
|
|
146
|
+
if (match) {
|
|
147
|
+
const varName = match[2];
|
|
148
|
+
// Check next line for immediate .json() without ok check
|
|
149
|
+
if (i + 1 < lines.length) {
|
|
150
|
+
const nextLine = lines[i + 1];
|
|
151
|
+
if (new RegExp(`${varName}\\.json\\s*\\(`).test(nextLine) && !/\.ok/.test(nextLine)) {
|
|
152
|
+
findings.push({
|
|
153
|
+
ruleId: 'BUG-RFIX-005', category: 'bugs', severity: 'high',
|
|
154
|
+
title: `Add response.ok check after fetch`,
|
|
155
|
+
file: fp, line: i + 1,
|
|
156
|
+
fix: {
|
|
157
|
+
type: 'replace',
|
|
158
|
+
old: lines[i],
|
|
159
|
+
new: `${lines[i]}\n${match[1]}if (!${varName}.ok) throw new Error(\`HTTP \${${varName}.status}: \${${varName}.statusText}\`);`,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return findings;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'BUG-RFIX-006',
|
|
172
|
+
category: 'bugs',
|
|
173
|
+
severity: 'medium',
|
|
174
|
+
confidence: 'definite',
|
|
175
|
+
title: 'export let → export const for non-reassigned exports',
|
|
176
|
+
check({ files }) {
|
|
177
|
+
const findings = [];
|
|
178
|
+
for (const [fp, content] of files) {
|
|
179
|
+
if (!isJSX(fp) && !fp.endsWith('.js') && !fp.endsWith('.mjs')) continue;
|
|
180
|
+
const lines = content.split('\n');
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
if (/^export\s+let\s+(\w+)\s*=/.test(lines[i])) {
|
|
183
|
+
const varName = lines[i].match(/export\s+let\s+(\w+)/)[1];
|
|
184
|
+
// Check if the variable is reassigned later
|
|
185
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
186
|
+
const reassignRe = new RegExp(`^\\s*${varName}\\s*=(?!=)`, 'm');
|
|
187
|
+
if (!reassignRe.test(rest)) {
|
|
188
|
+
findings.push({
|
|
189
|
+
ruleId: 'BUG-RFIX-006', category: 'bugs', severity: 'medium',
|
|
190
|
+
title: `export let "${varName}" → export const (never reassigned)`,
|
|
191
|
+
file: fp, line: i + 1,
|
|
192
|
+
fix: {
|
|
193
|
+
type: 'replace',
|
|
194
|
+
old: `export let ${varName}`,
|
|
195
|
+
new: `export const ${varName}`,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return findings;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
export default rules;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Bug detection: Ruby common patterns AI generators get wrong
|
|
2
|
+
function isRuby(f) { return f.endsWith('.rb') || f.endsWith('.erb'); }
|
|
3
|
+
|
|
4
|
+
const rules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'BUG-RUBY-001',
|
|
7
|
+
category: 'bugs',
|
|
8
|
+
severity: 'critical',
|
|
9
|
+
confidence: 'definite',
|
|
10
|
+
title: 'Mass assignment vulnerability — permit all params',
|
|
11
|
+
check({ files }) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const [fp, content] of files) {
|
|
14
|
+
if (!isRuby(fp)) continue;
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (/params\.permit!/.test(lines[i]) || /\.permit\(\s*!\s*\)/.test(lines[i])) {
|
|
18
|
+
findings.push({
|
|
19
|
+
ruleId: 'BUG-RUBY-001', category: 'bugs', severity: 'high',
|
|
20
|
+
title: 'params.permit! allows ALL parameters — mass assignment vulnerability',
|
|
21
|
+
description: 'permit! allows all params including admin, role, password. Always whitelist: params.require(:user).permit(:name, :email).',
|
|
22
|
+
file: fp, line: i + 1, fix: null,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'BUG-RUBY-002',
|
|
32
|
+
category: 'bugs',
|
|
33
|
+
severity: 'high',
|
|
34
|
+
confidence: 'likely',
|
|
35
|
+
title: 'N+1 query in loop',
|
|
36
|
+
check({ files }) {
|
|
37
|
+
const findings = [];
|
|
38
|
+
for (const [fp, content] of files) {
|
|
39
|
+
if (!isRuby(fp)) continue;
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
if (/\.each\s+do\s*\|/.test(lines[i]) || /\.map\s+do\s*\|/.test(lines[i])) {
|
|
43
|
+
let block = '';
|
|
44
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
45
|
+
block += lines[j] + '\n';
|
|
46
|
+
}
|
|
47
|
+
// Database queries inside loop
|
|
48
|
+
if (/\.(find|find_by|where|count|exists\?|first|last|pluck)\b/.test(block)) {
|
|
49
|
+
findings.push({
|
|
50
|
+
ruleId: 'BUG-RUBY-002', category: 'bugs', severity: 'high',
|
|
51
|
+
title: 'N+1 query — database call inside loop',
|
|
52
|
+
description: 'Each iteration makes a separate DB query. Use .includes(:association) or .eager_load() to batch queries.',
|
|
53
|
+
file: fp, line: i + 1, fix: null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return findings;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'BUG-RUBY-003',
|
|
64
|
+
category: 'bugs',
|
|
65
|
+
severity: 'high',
|
|
66
|
+
confidence: 'definite',
|
|
67
|
+
title: 'SQL injection via string interpolation',
|
|
68
|
+
check({ files }) {
|
|
69
|
+
const findings = [];
|
|
70
|
+
for (const [fp, content] of files) {
|
|
71
|
+
if (!isRuby(fp)) continue;
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
// .where("column = #{var}") — SQL injection
|
|
75
|
+
if (/\.where\s*\(\s*"[^"]*#\{/.test(lines[i]) || /\.where\s*\(\s*'[^']*#\{/.test(lines[i])) {
|
|
76
|
+
findings.push({
|
|
77
|
+
ruleId: 'BUG-RUBY-003', category: 'bugs', severity: 'high',
|
|
78
|
+
title: 'SQL injection — string interpolation in ActiveRecord .where()',
|
|
79
|
+
description: 'Use parameterized queries: .where("column = ?", value) or .where(column: value) instead of string interpolation.',
|
|
80
|
+
file: fp, line: i + 1, fix: null,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'BUG-RUBY-004',
|
|
90
|
+
category: 'bugs',
|
|
91
|
+
severity: 'medium',
|
|
92
|
+
confidence: 'likely',
|
|
93
|
+
title: 'Rescue without specific exception class',
|
|
94
|
+
check({ files }) {
|
|
95
|
+
const findings = [];
|
|
96
|
+
for (const [fp, content] of files) {
|
|
97
|
+
if (!isRuby(fp)) continue;
|
|
98
|
+
const lines = content.split('\n');
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
// rescue without exception class (catches StandardError) or rescue Exception (catches everything)
|
|
101
|
+
if (/^\s*rescue\s*$/.test(lines[i]) || /^\s*rescue\s*=>/.test(lines[i])) {
|
|
102
|
+
findings.push({
|
|
103
|
+
ruleId: 'BUG-RUBY-004', category: 'bugs', severity: 'medium',
|
|
104
|
+
title: 'Bare rescue catches StandardError — specify exception class',
|
|
105
|
+
description: 'Bare rescue catches too many exceptions. Use rescue SpecificError => e to handle only expected errors.',
|
|
106
|
+
file: fp, line: i + 1, fix: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (/^\s*rescue\s+Exception\b/.test(lines[i])) {
|
|
110
|
+
findings.push({
|
|
111
|
+
ruleId: 'BUG-RUBY-004', category: 'bugs', severity: 'medium',
|
|
112
|
+
title: 'rescue Exception catches EVERYTHING including SignalException and SystemExit',
|
|
113
|
+
description: 'rescue Exception catches Ctrl+C, kill signals, and syntax errors. Use rescue StandardError or a specific error class.',
|
|
114
|
+
file: fp, line: i + 1, fix: null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'BUG-RUBY-005',
|
|
124
|
+
category: 'bugs',
|
|
125
|
+
severity: 'high',
|
|
126
|
+
confidence: 'likely',
|
|
127
|
+
title: 'Unscoped find — users can access any record',
|
|
128
|
+
check({ files }) {
|
|
129
|
+
const findings = [];
|
|
130
|
+
for (const [fp, content] of files) {
|
|
131
|
+
if (!isRuby(fp)) continue;
|
|
132
|
+
if (!/controller/i.test(fp)) continue;
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
// Model.find(params[:id]) without scoping to current_user
|
|
136
|
+
if (/[A-Z]\w+\.find\s*\(\s*params\[/.test(lines[i])) {
|
|
137
|
+
const context = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
|
|
138
|
+
if (!/current_user|@user|authorize|policy|scope/.test(context)) {
|
|
139
|
+
findings.push({
|
|
140
|
+
ruleId: 'BUG-RUBY-005', category: 'bugs', severity: 'high',
|
|
141
|
+
title: 'Unscoped Model.find(params[:id]) — any user can access any record',
|
|
142
|
+
description: 'Use current_user.posts.find(params[:id]) instead of Post.find(params[:id]) to enforce authorization.',
|
|
143
|
+
file: fp, line: i + 1, fix: null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findings;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'BUG-RUBY-006',
|
|
154
|
+
category: 'bugs',
|
|
155
|
+
severity: 'medium',
|
|
156
|
+
confidence: 'definite',
|
|
157
|
+
title: 'html_safe on user input — XSS vulnerability',
|
|
158
|
+
check({ files }) {
|
|
159
|
+
const findings = [];
|
|
160
|
+
for (const [fp, content] of files) {
|
|
161
|
+
if (!isRuby(fp)) continue;
|
|
162
|
+
const lines = content.split('\n');
|
|
163
|
+
for (let i = 0; i < lines.length; i++) {
|
|
164
|
+
if (/\.(html_safe|raw)\b/.test(lines[i])) {
|
|
165
|
+
// Check if it involves user input or params
|
|
166
|
+
if (/params|@\w+|user|input|content|body|text|message|comment|name|title/.test(lines[i])) {
|
|
167
|
+
findings.push({
|
|
168
|
+
ruleId: 'BUG-RUBY-006', category: 'bugs', severity: 'medium',
|
|
169
|
+
title: '.html_safe or raw() on potentially user-controlled content — XSS',
|
|
170
|
+
description: 'html_safe bypasses Rails XSS protection. Use sanitize() helper for user content that needs HTML.',
|
|
171
|
+
file: fp, line: i + 1, fix: null,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return findings;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
export default rules;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Bug detection: Shell script bugs that AI generators commonly produce
|
|
2
|
+
function isSh(f) { return f.endsWith('.sh') || f.endsWith('.bash') || f.endsWith('.zsh'); }
|
|
3
|
+
|
|
4
|
+
const rules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'BUG-SH-001',
|
|
7
|
+
category: 'bugs',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
confidence: 'definite',
|
|
10
|
+
title: 'Unquoted variable expansion — word splitting and globbing bugs',
|
|
11
|
+
check({ files }) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const [fp, content] of files) {
|
|
14
|
+
if (!isSh(fp)) continue;
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
if (/^\s*#/.test(line)) continue;
|
|
19
|
+
// $VAR without quotes in commands (not inside [[ ]] or assignments)
|
|
20
|
+
if (/\s\$\w+\b/.test(line) && !/"\$/.test(line) && !/'\$/.test(line) && !/\[\[/.test(line) && !/^\s*\w+=/.test(line)) {
|
|
21
|
+
if (/\b(cp|mv|rm|cat|echo|cd|mkdir|chmod|chown|ls|grep|find|test)\b/.test(line)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
ruleId: 'BUG-SH-001', category: 'bugs', severity: 'high',
|
|
24
|
+
title: 'Unquoted variable in command — breaks on spaces and special chars',
|
|
25
|
+
description: 'Always quote variables: "$VAR" not $VAR. Unquoted variables undergo word splitting and glob expansion.',
|
|
26
|
+
file: fp, line: i + 1, fix: null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return findings;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'BUG-SH-002',
|
|
37
|
+
category: 'bugs',
|
|
38
|
+
severity: 'high',
|
|
39
|
+
confidence: 'definite',
|
|
40
|
+
title: 'Missing error handling — no set -e or error checks',
|
|
41
|
+
check({ files }) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const [fp, content] of files) {
|
|
44
|
+
if (!isSh(fp)) continue;
|
|
45
|
+
const hasSetE = /set\s+-e|set\s+-o\s+errexit/.test(content);
|
|
46
|
+
const hasTraps = /trap\s+/.test(content);
|
|
47
|
+
const lineCount = content.split('\n').length;
|
|
48
|
+
if (!hasSetE && !hasTraps && lineCount > 10) {
|
|
49
|
+
// Check if there are any error checks at all
|
|
50
|
+
const hasChecks = /\|\|\s*(exit|echo|die)|if\s+\[\s*\$\?\s*-ne\s*0|&&\s*\{/.test(content);
|
|
51
|
+
if (!hasChecks) {
|
|
52
|
+
findings.push({
|
|
53
|
+
ruleId: 'BUG-SH-002', category: 'bugs', severity: 'high',
|
|
54
|
+
title: 'Shell script without error handling — failures are silent',
|
|
55
|
+
description: 'Add "set -euo pipefail" at the top of the script. Without it, commands that fail are silently ignored and the script continues.',
|
|
56
|
+
file: fp, line: 1, fix: null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return findings;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'BUG-SH-003',
|
|
66
|
+
category: 'bugs',
|
|
67
|
+
severity: 'critical',
|
|
68
|
+
confidence: 'definite',
|
|
69
|
+
title: 'rm -rf with variable that could be empty',
|
|
70
|
+
check({ files }) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
for (const [fp, content] of files) {
|
|
73
|
+
if (!isSh(fp)) continue;
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
// rm -rf $VAR/ or rm -rf ${VAR}/ — if VAR is empty, this becomes rm -rf /
|
|
77
|
+
if (/rm\s+-rf?\s+\$\w+\/|rm\s+-rf?\s+\$\{\w+\}\//.test(lines[i])) {
|
|
78
|
+
// Check if variable is validated before
|
|
79
|
+
const varName = lines[i].match(/\$\{?(\w+)\}?\//)[1];
|
|
80
|
+
const before = content.split('\n').slice(0, i).join('\n');
|
|
81
|
+
if (!new RegExp(`if\\s+\\[.*-[nz].*${varName}|\\$\\{${varName}:?-`).test(before)) {
|
|
82
|
+
findings.push({
|
|
83
|
+
ruleId: 'BUG-SH-003', category: 'bugs', severity: 'high',
|
|
84
|
+
title: `rm -rf with unchecked variable $${varName} — could delete root if empty`,
|
|
85
|
+
description: 'If the variable is empty, "rm -rf $VAR/" becomes "rm -rf /". Always check: ${VAR:?Variable not set} or test -n "$VAR".',
|
|
86
|
+
file: fp, line: i + 1, fix: null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'BUG-SH-004',
|
|
97
|
+
category: 'bugs',
|
|
98
|
+
severity: 'medium',
|
|
99
|
+
confidence: 'definite',
|
|
100
|
+
title: 'Using [ ] instead of [[ ]] for string comparison',
|
|
101
|
+
check({ files }) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
for (const [fp, content] of files) {
|
|
104
|
+
if (!isSh(fp)) continue;
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
// [ "$var" = "val" ] — should be [[ ]]
|
|
108
|
+
if (/\[\s+"?\$\w+"?\s*=\s+/.test(lines[i]) && !/\[\[/.test(lines[i])) {
|
|
109
|
+
findings.push({
|
|
110
|
+
ruleId: 'BUG-SH-004', category: 'bugs', severity: 'medium',
|
|
111
|
+
title: '[ ] for string comparison — use [[ ]] to avoid word splitting issues',
|
|
112
|
+
description: '[[ ]] is safer: no word splitting, no glob expansion, supports && || and regex. Use [[ ]] in bash scripts.',
|
|
113
|
+
file: fp, line: i + 1, fix: null,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'BUG-SH-005',
|
|
123
|
+
category: 'bugs',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
confidence: 'likely',
|
|
126
|
+
title: 'Parsing ls output — brittle and breaks on special filenames',
|
|
127
|
+
check({ files }) {
|
|
128
|
+
const findings = [];
|
|
129
|
+
for (const [fp, content] of files) {
|
|
130
|
+
if (!isSh(fp)) continue;
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
if (/for\s+\w+\s+in\s+\$\(ls\b/.test(lines[i]) || /\bls\b.*\|\s*(while|xargs|awk|sed|grep)/.test(lines[i])) {
|
|
134
|
+
findings.push({
|
|
135
|
+
ruleId: 'BUG-SH-005', category: 'bugs', severity: 'high',
|
|
136
|
+
title: 'Parsing ls output — breaks on filenames with spaces/newlines',
|
|
137
|
+
description: 'Use glob patterns (for f in *.txt) or find with -exec/-print0 instead of parsing ls output.',
|
|
138
|
+
file: fp, line: i + 1, fix: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return findings;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'BUG-SH-006',
|
|
148
|
+
category: 'bugs',
|
|
149
|
+
severity: 'medium',
|
|
150
|
+
confidence: 'likely',
|
|
151
|
+
title: 'TOCTOU race — test then use pattern',
|
|
152
|
+
check({ files }) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
for (const [fp, content] of files) {
|
|
155
|
+
if (!isSh(fp)) continue;
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
// if [ -f "$file" ]; then ... cat/rm/mv "$file"
|
|
159
|
+
if (/if\s+\[\[?\s+-[fde]\s+/.test(lines[i])) {
|
|
160
|
+
const fileVar = lines[i].match(/\-[fde]\s+"?\$\{?(\w+)\}?"?/)?.[1];
|
|
161
|
+
if (!fileVar) continue;
|
|
162
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
163
|
+
if (new RegExp(`\\b(rm|mv|cat|cp|chmod|chown)\\b.*\\$\\{?${fileVar}\\}?`).test(lines[j])) {
|
|
164
|
+
findings.push({
|
|
165
|
+
ruleId: 'BUG-SH-006', category: 'bugs', severity: 'medium',
|
|
166
|
+
title: 'TOCTOU race — file could change between test and use',
|
|
167
|
+
description: 'File may be deleted/modified between the test and use. For critical operations, use atomic operations or lock files.',
|
|
168
|
+
file: fp, line: i + 1, fix: null,
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findings;
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
export default rules;
|