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,283 @@
1
+ /**
2
+ * Prototype Pollution Detection Rules (SEC-PP-001 through SEC-PP-010)
3
+ *
4
+ * Detects patterns that allow attackers to inject properties into
5
+ * Object.prototype, leading to DoS, property injection, or RCE.
6
+ */
7
+
8
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
9
+ const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
10
+
11
+ const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|node_modules|vendor|dist|build)[/\\]/i;
12
+ const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
13
+
14
+ function scanLines(content, regex, file, rule) {
15
+ const findings = [];
16
+ const lines = content.split('\n');
17
+ for (let i = 0; i < lines.length; i++) {
18
+ const line = lines[i];
19
+ if (COMMENT_LINE.test(line)) continue;
20
+ if (regex.test(line)) {
21
+ findings.push({
22
+ ruleId: rule.id,
23
+ category: rule.category,
24
+ severity: rule.severity,
25
+ title: rule.title,
26
+ description: rule.description,
27
+ confidence: rule.confidence,
28
+ file,
29
+ line: i + 1,
30
+ fix: rule.fix || null,
31
+ });
32
+ }
33
+ }
34
+ return findings;
35
+ }
36
+
37
+ const rules = [
38
+ // SEC-PP-001: Direct __proto__ assignment
39
+ {
40
+ id: 'SEC-PP-001',
41
+ category: 'security',
42
+ severity: 'critical',
43
+ confidence: 'definite',
44
+ title: 'Direct __proto__ Property Access',
45
+ description:
46
+ 'Accessing or assigning __proto__ can modify the prototype chain of all objects, leading to prototype pollution.',
47
+ fix: { suggestion: 'Use Object.create(null) for lookup objects and validate property names against a deny list.' },
48
+ check({ files }) {
49
+ const findings = [];
50
+ const pattern = /\.__proto__\s*[=\[]/;
51
+ for (const [path, content] of files) {
52
+ if (SKIP_PATH.test(path)) continue;
53
+ if (isJS(path)) {
54
+ findings.push(...scanLines(content, pattern, path, this));
55
+ }
56
+ }
57
+ return findings;
58
+ },
59
+ },
60
+
61
+ // SEC-PP-002: Object.assign from user input
62
+ {
63
+ id: 'SEC-PP-002',
64
+ category: 'security',
65
+ severity: 'high',
66
+ confidence: 'likely',
67
+ title: 'Object.assign with User-Controlled Source',
68
+ description:
69
+ 'Object.assign merges all enumerable properties including __proto__. If the source is user-controlled (req.body, req.query), an attacker can pollute the prototype.',
70
+ fix: { suggestion: 'Sanitize input before merging or use a safe merge utility that skips __proto__ and constructor.' },
71
+ check({ files }) {
72
+ const findings = [];
73
+ const pattern = /Object\.assign\s*\([^)]*(?:req\.body|req\.query|req\.params|userInput|userData|payload|input)/;
74
+ for (const [path, content] of files) {
75
+ if (SKIP_PATH.test(path)) continue;
76
+ if (isJS(path)) {
77
+ findings.push(...scanLines(content, pattern, path, this));
78
+ }
79
+ }
80
+ return findings;
81
+ },
82
+ },
83
+
84
+ // SEC-PP-003: Recursive merge without prototype guard
85
+ {
86
+ id: 'SEC-PP-003',
87
+ category: 'security',
88
+ severity: 'high',
89
+ confidence: 'likely',
90
+ title: 'Recursive/Deep Merge Without Prototype Pollution Guard',
91
+ description:
92
+ 'Custom deep merge or lodash.merge with user-controlled input can set properties on Object.prototype via __proto__ or constructor.prototype paths.',
93
+ fix: { suggestion: 'Use lodash >=4.17.12 or add explicit checks for __proto__, constructor, and prototype keys.' },
94
+ check({ files }) {
95
+ const findings = [];
96
+ const pattern = /(?:deepMerge|merge|deepExtend|extend|defaultsDeep)\s*\([^)]*(?:req\.body|req\.query|req\.params|userInput|userData|payload|input)/;
97
+ for (const [path, content] of files) {
98
+ if (SKIP_PATH.test(path)) continue;
99
+ if (isJS(path)) {
100
+ findings.push(...scanLines(content, pattern, path, this));
101
+ }
102
+ }
103
+ return findings;
104
+ },
105
+ },
106
+
107
+ // SEC-PP-004: constructor.prototype access
108
+ {
109
+ id: 'SEC-PP-004',
110
+ category: 'security',
111
+ severity: 'critical',
112
+ confidence: 'definite',
113
+ title: 'Constructor Prototype Access Pattern',
114
+ description:
115
+ 'Accessing constructor.prototype allows modification of the prototype chain, similar to __proto__ pollution.',
116
+ fix: { suggestion: 'Deny "constructor" as a valid property key in user input validation.' },
117
+ check({ files }) {
118
+ const findings = [];
119
+ const pattern = /\[['"]constructor['"]\]\s*\[\s*['"]prototype['"]\s*\]/;
120
+ for (const [path, content] of files) {
121
+ if (SKIP_PATH.test(path)) continue;
122
+ if (isJS(path)) {
123
+ findings.push(...scanLines(content, pattern, path, this));
124
+ }
125
+ }
126
+ return findings;
127
+ },
128
+ },
129
+
130
+ // SEC-PP-005: Dynamic property assignment from user input
131
+ {
132
+ id: 'SEC-PP-005',
133
+ category: 'security',
134
+ severity: 'high',
135
+ confidence: 'likely',
136
+ title: 'Dynamic Property Assignment from User Input',
137
+ description:
138
+ 'Setting object properties using bracket notation with user-controlled keys (obj[key] = value) can lead to prototype pollution if key is "__proto__".',
139
+ fix: { suggestion: 'Validate that property keys are not "__proto__", "constructor", or "prototype" before assignment.' },
140
+ check({ files }) {
141
+ const findings = [];
142
+ const pattern = /\w+\[(?:req\.body|req\.query|req\.params|key|prop|name|field|attr)\s*(?:\.\w+)?\s*\]\s*=/;
143
+ for (const [path, content] of files) {
144
+ if (SKIP_PATH.test(path)) continue;
145
+ if (isJS(path)) {
146
+ findings.push(...scanLines(content, pattern, path, this));
147
+ }
148
+ }
149
+ return findings;
150
+ },
151
+ },
152
+
153
+ // SEC-PP-006: Spread operator with unsanitized user input
154
+ {
155
+ id: 'SEC-PP-006',
156
+ category: 'security',
157
+ severity: 'medium',
158
+ confidence: 'likely',
159
+ title: 'Spread Operator with Unsanitized User Input',
160
+ description:
161
+ 'Using the spread operator (...req.body) to create new objects can carry over __proto__ properties from malicious input in some environments.',
162
+ fix: { suggestion: 'Destructure only known properties or use a schema validation library like Zod or Joi.' },
163
+ check({ files }) {
164
+ const findings = [];
165
+ const pattern = /\.\.\.(?:req\.body|req\.query|req\.params|userInput|userData|payload)\b/;
166
+ for (const [path, content] of files) {
167
+ if (SKIP_PATH.test(path)) continue;
168
+ if (isJS(path)) {
169
+ findings.push(...scanLines(content, pattern, path, this));
170
+ }
171
+ }
172
+ return findings;
173
+ },
174
+ },
175
+
176
+ // SEC-PP-007: JSON.parse without prototype sanitization
177
+ {
178
+ id: 'SEC-PP-007',
179
+ category: 'security',
180
+ severity: 'medium',
181
+ confidence: 'likely',
182
+ title: 'JSON.parse Result Used in Object Merge Without Sanitization',
183
+ description:
184
+ 'JSON.parse preserves __proto__ keys in parsed objects. Merging the result into existing objects can pollute prototypes.',
185
+ fix: { suggestion: 'Use a JSON.parse reviver to strip dangerous keys, or use a safe-merge utility.' },
186
+ check({ files }) {
187
+ const findings = [];
188
+ const pattern = /Object\.assign\s*\([^)]*JSON\.parse|(?:merge|extend|defaults)\s*\([^)]*JSON\.parse/;
189
+ for (const [path, content] of files) {
190
+ if (SKIP_PATH.test(path)) continue;
191
+ if (isJS(path)) {
192
+ findings.push(...scanLines(content, pattern, path, this));
193
+ }
194
+ }
195
+ return findings;
196
+ },
197
+ },
198
+
199
+ // SEC-PP-008: Lodash vulnerable merge functions
200
+ {
201
+ id: 'SEC-PP-008',
202
+ category: 'security',
203
+ severity: 'high',
204
+ confidence: 'likely',
205
+ title: 'Lodash Merge/DefaultsDeep with External Input',
206
+ description:
207
+ 'lodash.merge, _.defaultsDeep, and _.set are known prototype pollution vectors (CVE-2018-16487, CVE-2019-10744). Ensure lodash >= 4.17.12.',
208
+ fix: { suggestion: 'Update lodash to >= 4.17.12 and validate input before passing to merge functions.' },
209
+ check({ files }) {
210
+ const findings = [];
211
+ const pattern = /(?:_\.merge|_\.defaultsDeep|_\.set|lodash\.merge|lodash\.defaultsDeep)\s*\([^)]*(?:req\.|input|body|query|params|payload|userData)/;
212
+ for (const [path, content] of files) {
213
+ if (SKIP_PATH.test(path)) continue;
214
+ if (isJS(path)) {
215
+ findings.push(...scanLines(content, pattern, path, this));
216
+ }
217
+ }
218
+ return findings;
219
+ },
220
+ },
221
+
222
+ // SEC-PP-009: Object.defineProperty with user-controlled descriptor
223
+ {
224
+ id: 'SEC-PP-009',
225
+ category: 'security',
226
+ severity: 'high',
227
+ confidence: 'likely',
228
+ title: 'Object.defineProperty with User-Controlled Descriptor',
229
+ description:
230
+ 'Object.defineProperty with user-controlled property name or descriptor can be used to modify prototype properties.',
231
+ fix: { suggestion: 'Validate property names and descriptors against an allowlist before using Object.defineProperty.' },
232
+ check({ files }) {
233
+ const findings = [];
234
+ const pattern = /Object\.definePropert(?:y|ies)\s*\([^)]*(?:req\.body|req\.query|req\.params|userInput|input|payload)/;
235
+ for (const [path, content] of files) {
236
+ if (SKIP_PATH.test(path)) continue;
237
+ if (isJS(path)) {
238
+ findings.push(...scanLines(content, pattern, path, this));
239
+ }
240
+ }
241
+ return findings;
242
+ },
243
+ },
244
+
245
+ // SEC-PP-010: Missing Object.freeze on prototype-sensitive objects
246
+ {
247
+ id: 'SEC-PP-010',
248
+ category: 'security',
249
+ severity: 'medium',
250
+ confidence: 'likely',
251
+ title: 'Prototype-Sensitive Config Object Not Frozen',
252
+ description:
253
+ 'Configuration or default objects used as prototypes should be frozen with Object.freeze() to prevent prototype pollution.',
254
+ fix: { suggestion: 'Use Object.freeze() on configuration/defaults objects, or use Object.create(null) for dictionary objects.' },
255
+ check({ files }) {
256
+ const findings = [];
257
+ const configPattern = /(?:const|let|var)\s+(?:defaults|config|options|settings|baseConfig)\s*=\s*\{/;
258
+ const freezePattern = /Object\.freeze\s*\(\s*(?:defaults|config|options|settings|baseConfig)\s*\)/;
259
+ for (const [path, content] of files) {
260
+ if (SKIP_PATH.test(path)) continue;
261
+ if (!isJS(path)) continue;
262
+ if (configPattern.test(content) && !freezePattern.test(content)) {
263
+ // Check if these objects are later merged with user input
264
+ if (/(?:merge|assign|extend|defaults)\s*\(/.test(content) && /(?:req\.|input|body|query|params)/.test(content)) {
265
+ findings.push({
266
+ ruleId: this.id,
267
+ category: this.category,
268
+ severity: this.severity,
269
+ title: this.title,
270
+ description: this.description,
271
+ confidence: this.confidence,
272
+ file: path,
273
+ fix: this.fix || null,
274
+ });
275
+ }
276
+ }
277
+ }
278
+ return findings;
279
+ },
280
+ },
281
+ ];
282
+
283
+ export default rules;
@@ -0,0 +1,208 @@
1
+ const rules = [
2
+ // SEC-RL-001
3
+ {
4
+ id: 'SEC-RL-001', category: 'security', severity: 'high', confidence: 'likely',
5
+ title: 'No rate limiting on API',
6
+ check({ files, stack }) {
7
+ const findings = [];
8
+ if (stack.runtime !== 'node') return findings;
9
+ const deps = { ...stack.dependencies, ...stack.devDependencies };
10
+ const hasRateLimiter = Object.keys(deps).some(d =>
11
+ ['express-rate-limit', 'rate-limiter-flexible', '@upstash/ratelimit', 'bottleneck', 'express-slow-down'].includes(d)
12
+ );
13
+ if (hasRateLimiter) return findings;
14
+
15
+ const hasApiRoutes = [...files.keys()].some(f => f.includes('/api/') || f.includes('routes'));
16
+ const hasRateLimitCode = [...files.values()].some(c =>
17
+ c.includes('rateLimit') || c.includes('rateLimiter') || c.includes('rate-limit') || c.includes('throttle')
18
+ );
19
+ if (hasApiRoutes && !hasRateLimitCode) {
20
+ findings.push({
21
+ ruleId: 'SEC-RL-001', category: 'security', severity: 'high',
22
+ title: 'No rate limiting detected on API — vulnerable to abuse and DDoS',
23
+ description: 'Add express-rate-limit, @upstash/ratelimit, or similar.',
24
+ fix: null,
25
+ });
26
+ }
27
+ return findings;
28
+ },
29
+ },
30
+
31
+ // SEC-RL-002
32
+ {
33
+ id: 'SEC-RL-002', category: 'security', severity: 'critical', confidence: 'definite',
34
+ title: 'No rate limiting on login endpoint',
35
+ check({ files, stack }) {
36
+ const findings = [];
37
+ if (stack.runtime !== 'node') return findings;
38
+
39
+ for (const [fp, content] of files) {
40
+ if (fp.includes('test') || fp.includes('spec')) continue;
41
+ if (content.match(/(?:\/api\/(?:auth\/)?login|\/login|\/signin|\/sign-in)/i) &&
42
+ content.match(/\.(post|all)\s*\(/) &&
43
+ !content.includes('rateLimit') && !content.includes('rateLimiter') && !content.includes('throttle')) {
44
+ findings.push({
45
+ ruleId: 'SEC-RL-002', category: 'security', severity: 'critical',
46
+ title: 'Login endpoint without rate limiting — brute force attacks possible',
47
+ file: fp, fix: null,
48
+ });
49
+ break;
50
+ }
51
+ }
52
+ return findings;
53
+ },
54
+ },
55
+
56
+ // SEC-RL-003
57
+ {
58
+ id: 'SEC-RL-003', category: 'security', severity: 'high', confidence: 'likely',
59
+ title: 'No rate limiting on registration',
60
+ check({ files, stack }) {
61
+ const findings = [];
62
+ if (stack.runtime !== 'node') return findings;
63
+
64
+ for (const [fp, content] of files) {
65
+ if (fp.includes('test') || fp.includes('spec')) continue;
66
+ if (content.match(/(?:\/api\/(?:auth\/)?(?:register|signup|sign-up))/i) &&
67
+ content.match(/\.(post|all)\s*\(/) &&
68
+ !content.includes('rateLimit') && !content.includes('rateLimiter')) {
69
+ findings.push({
70
+ ruleId: 'SEC-RL-003', category: 'security', severity: 'high',
71
+ title: 'Registration endpoint without rate limiting — spam account creation possible',
72
+ file: fp, fix: null,
73
+ });
74
+ break;
75
+ }
76
+ }
77
+ return findings;
78
+ },
79
+ },
80
+
81
+ // SEC-RL-004
82
+ {
83
+ id: 'SEC-RL-004', category: 'security', severity: 'high', confidence: 'likely',
84
+ title: 'No rate limiting on password reset',
85
+ check({ files, stack }) {
86
+ const findings = [];
87
+ if (stack.runtime !== 'node') return findings;
88
+
89
+ for (const [fp, content] of files) {
90
+ if (fp.includes('test') || fp.includes('spec')) continue;
91
+ if (content.match(/(?:\/api\/(?:auth\/)?(?:reset|forgot|password))/i) &&
92
+ content.match(/\.(post|all)\s*\(/) &&
93
+ !content.includes('rateLimit') && !content.includes('rateLimiter')) {
94
+ findings.push({
95
+ ruleId: 'SEC-RL-004', category: 'security', severity: 'high',
96
+ title: 'Password reset endpoint without rate limiting — email bombing possible',
97
+ file: fp, fix: null,
98
+ });
99
+ break;
100
+ }
101
+ }
102
+ return findings;
103
+ },
104
+ },
105
+
106
+ // SEC-RL-005
107
+ {
108
+ id: 'SEC-RL-005', category: 'security', severity: 'medium', confidence: 'likely',
109
+ title: 'Rate limit too high',
110
+ check({ files }) {
111
+ const findings = [];
112
+ for (const [fp, content] of files) {
113
+ if (fp.includes('test')) continue;
114
+ const match = content.match(/(?:max|limit)\s*[:=]\s*(\d+)/);
115
+ if (match && parseInt(match[1]) > 1000) {
116
+ findings.push({
117
+ ruleId: 'SEC-RL-005', category: 'security', severity: 'medium',
118
+ title: `Rate limit set very high (${match[1]} requests) — may not effectively prevent abuse`,
119
+ file: fp, fix: null,
120
+ });
121
+ }
122
+ }
123
+ return findings;
124
+ },
125
+ },
126
+
127
+ // SEC-RL-006
128
+ {
129
+ id: 'SEC-RL-006', category: 'security', severity: 'medium', confidence: 'likely',
130
+ title: 'Rate limit by IP only',
131
+ check({ files }) {
132
+ const findings = [];
133
+ for (const [fp, content] of files) {
134
+ if (fp.includes('test')) continue;
135
+ if (content.includes('rateLimit') || content.includes('RateLimit')) {
136
+ if (content.includes('keyGenerator') || content.includes('key:')) {
137
+ // Custom key generator — likely not just IP
138
+ } else if (content.includes('express-rate-limit')) {
139
+ // express-rate-limit is a standard, accepted IP-based rate limiter — don't flag
140
+ } else if (content.includes('rateLimit(') || content.includes('new RateLimiter')) {
141
+ findings.push({
142
+ ruleId: 'SEC-RL-006', category: 'security', severity: 'medium',
143
+ title: 'Rate limiting appears to be IP-only — bypassable with proxies/VPNs',
144
+ description: 'Consider adding user ID, API key, or fingerprint to the rate limit key.',
145
+ file: fp, fix: null,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ return findings;
151
+ },
152
+ },
153
+
154
+ // SEC-RL-007
155
+ {
156
+ id: 'SEC-RL-007', category: 'security', severity: 'high', confidence: 'likely',
157
+ title: 'No rate limiting on file upload',
158
+ check({ files, stack }) {
159
+ const findings = [];
160
+ if (stack.runtime !== 'node') return findings;
161
+ const hasUpload = [...files.values()].some(c =>
162
+ c.includes('multer') || c.includes('upload') || c.includes('formidable') || c.includes('busboy')
163
+ );
164
+ if (hasUpload) {
165
+ const hasRateLimitOnUpload = [...files.values()].some(c =>
166
+ (c.includes('upload') || c.includes('multer')) && (c.includes('rateLimit') || c.includes('rateLimiter'))
167
+ );
168
+ // If upload routes are protected by auth middleware, rate limiting is handled at auth layer
169
+ const hasAuthOnUpload = [...files.values()].some(c =>
170
+ (c.includes('upload') || c.includes('multer')) &&
171
+ /(?:authenticate|auth|verifyToken|protect|isAuthenticated|requireAuth|ensureAuth|passport\.authenticate|requireLogin|checkAuth)/i.test(c)
172
+ );
173
+ if (!hasRateLimitOnUpload && !hasAuthOnUpload) {
174
+ findings.push({
175
+ ruleId: 'SEC-RL-007', category: 'security', severity: 'high',
176
+ title: 'File upload endpoints without rate limiting — storage abuse possible',
177
+ fix: null,
178
+ });
179
+ }
180
+ }
181
+ return findings;
182
+ },
183
+ },
184
+
185
+ // SEC-RL-008
186
+ {
187
+ id: 'SEC-RL-008', category: 'security', severity: 'medium', confidence: 'likely',
188
+ title: 'No rate limiting on API key generation',
189
+ check({ files }) {
190
+ const findings = [];
191
+ for (const [fp, content] of files) {
192
+ if (fp.includes('test')) continue;
193
+ if (content.match(/(?:generate|create).*(?:apiKey|api_key|token)/i) &&
194
+ content.match(/\.(post|all)\s*\(/) &&
195
+ !content.includes('rateLimit')) {
196
+ findings.push({
197
+ ruleId: 'SEC-RL-008', category: 'security', severity: 'medium',
198
+ title: 'API key generation endpoint without rate limiting',
199
+ file: fp, fix: null,
200
+ });
201
+ }
202
+ }
203
+ return findings;
204
+ },
205
+ },
206
+ ];
207
+
208
+ export default rules;