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,1061 @@
1
+ /**
2
+ * Ruby Security Rules (SEC-RUBY-001 through SEC-RUBY-060)
3
+ *
4
+ * Each rule scans Ruby source files for security vulnerabilities.
5
+ */
6
+
7
+ const isRuby = (f) => f.endsWith('.rb');
8
+
9
+ const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|__snapshots__|node_modules|vendor|dist|build)[/\\]/i;
10
+ const COMMENT_LINE = /^\s*(#)/;
11
+
12
+ function scanLines(content, regex, file, rule) {
13
+ const findings = [];
14
+ const lines = content.split('\n');
15
+ for (let i = 0; i < lines.length; i++) {
16
+ const line = lines[i];
17
+ if (COMMENT_LINE.test(line)) continue;
18
+ if (regex.test(line)) {
19
+ findings.push({
20
+ ruleId: rule.id,
21
+ category: rule.category,
22
+ severity: rule.severity,
23
+ title: rule.title,
24
+ description: rule.description,
25
+ confidence: rule.confidence,
26
+ file,
27
+ line: i + 1,
28
+ fix: rule.fix || null,
29
+ });
30
+ }
31
+ }
32
+ return findings;
33
+ }
34
+
35
+ function checkRuby(rule, files, ...patterns) {
36
+ const findings = [];
37
+ for (const [path, content] of files) {
38
+ if (SKIP_PATH.test(path)) continue;
39
+ if (isRuby(path)) {
40
+ for (const p of patterns) {
41
+ findings.push(...scanLines(content, p, path, rule));
42
+ }
43
+ }
44
+ }
45
+ return findings;
46
+ }
47
+
48
+ const rules = [
49
+ // ===========================================================================
50
+ // Rails SQL Injection (001–010)
51
+ // ===========================================================================
52
+
53
+ // SEC-RUBY-001: SQL injection via string interpolation in where clause
54
+ {
55
+ id: 'SEC-RUBY-001',
56
+ category: 'security',
57
+ severity: 'critical',
58
+ confidence: 'likely',
59
+ title: 'SQL Injection via String Interpolation in where()',
60
+ description: 'Using string interpolation inside ActiveRecord where() allows SQL injection.',
61
+ fix: { suggestion: 'Use parameterized where: where("col = ?", value) or where(col: value).' },
62
+ check({ files }) {
63
+ return checkRuby(this, files,
64
+ /\.where\s*\(\s*["'].*#\{/,
65
+ );
66
+ },
67
+ },
68
+
69
+ // SEC-RUBY-002: SQL injection via string concatenation in where clause
70
+ {
71
+ id: 'SEC-RUBY-002',
72
+ category: 'security',
73
+ severity: 'critical',
74
+ confidence: 'likely',
75
+ title: 'SQL Injection via String Concatenation in where()',
76
+ description: 'Concatenating user input into ActiveRecord where() enables SQL injection.',
77
+ fix: { suggestion: 'Use parameterized queries instead of string concatenation.' },
78
+ check({ files }) {
79
+ return checkRuby(this, files,
80
+ /\.where\s*\(\s*["'].*["']\s*\+/,
81
+ );
82
+ },
83
+ },
84
+
85
+ // SEC-RUBY-003: Raw SQL execution with interpolation
86
+ {
87
+ id: 'SEC-RUBY-003',
88
+ category: 'security',
89
+ severity: 'critical',
90
+ confidence: 'likely',
91
+ title: 'Raw SQL with String Interpolation',
92
+ description: 'Using execute() or select_all() with interpolated strings allows SQL injection.',
93
+ fix: { suggestion: 'Use sanitize_sql_array or parameterized queries.' },
94
+ check({ files }) {
95
+ return checkRuby(this, files,
96
+ /(?:execute|select_all|select_one|select_value)\s*\(\s*["'].*#\{/,
97
+ );
98
+ },
99
+ },
100
+
101
+ // SEC-RUBY-004: SQL injection via find_by_sql
102
+ {
103
+ id: 'SEC-RUBY-004',
104
+ category: 'security',
105
+ severity: 'critical',
106
+ confidence: 'likely',
107
+ title: 'SQL Injection via find_by_sql',
108
+ description: 'Using find_by_sql with string interpolation allows SQL injection.',
109
+ fix: { suggestion: 'Use parameterized find_by_sql: find_by_sql(["SELECT ... WHERE id = ?", id]).' },
110
+ check({ files }) {
111
+ return checkRuby(this, files,
112
+ /find_by_sql\s*\(\s*["'].*#\{/,
113
+ );
114
+ },
115
+ },
116
+
117
+ // SEC-RUBY-005: SQL injection via order clause
118
+ {
119
+ id: 'SEC-RUBY-005',
120
+ category: 'security',
121
+ severity: 'high',
122
+ confidence: 'likely',
123
+ title: 'SQL Injection via order()',
124
+ description: 'Passing user input directly to order() can allow SQL injection.',
125
+ fix: { suggestion: 'Whitelist allowed columns and directions for ordering.' },
126
+ check({ files }) {
127
+ return checkRuby(this, files,
128
+ /\.order\s*\(\s*["'].*#\{/,
129
+ );
130
+ },
131
+ },
132
+
133
+ // SEC-RUBY-006: SQL injection via group clause
134
+ {
135
+ id: 'SEC-RUBY-006',
136
+ category: 'security',
137
+ severity: 'high',
138
+ confidence: 'likely',
139
+ title: 'SQL Injection via group()',
140
+ description: 'Passing user input to group() enables SQL injection.',
141
+ fix: { suggestion: 'Whitelist allowed column names for grouping.' },
142
+ check({ files }) {
143
+ return checkRuby(this, files,
144
+ /\.group\s*\(\s*["'].*#\{/,
145
+ );
146
+ },
147
+ },
148
+
149
+ // SEC-RUBY-007: SQL injection via pluck
150
+ {
151
+ id: 'SEC-RUBY-007',
152
+ category: 'security',
153
+ severity: 'high',
154
+ confidence: 'likely',
155
+ title: 'SQL Injection via pluck()',
156
+ description: 'Passing user input to pluck() can lead to SQL injection.',
157
+ fix: { suggestion: 'Whitelist column names before passing to pluck.' },
158
+ check({ files }) {
159
+ return checkRuby(this, files,
160
+ /\.pluck\s*\(\s*["'].*#\{/,
161
+ );
162
+ },
163
+ },
164
+
165
+ // SEC-RUBY-008: SQL injection via having clause
166
+ {
167
+ id: 'SEC-RUBY-008',
168
+ category: 'security',
169
+ severity: 'high',
170
+ confidence: 'likely',
171
+ title: 'SQL Injection via having()',
172
+ description: 'Using string interpolation in having() allows SQL injection.',
173
+ fix: { suggestion: 'Use parameterized having: having("count > ?", value).' },
174
+ check({ files }) {
175
+ return checkRuby(this, files,
176
+ /\.having\s*\(\s*["'].*#\{/,
177
+ );
178
+ },
179
+ },
180
+
181
+ // SEC-RUBY-009: SQL injection via joins
182
+ {
183
+ id: 'SEC-RUBY-009',
184
+ category: 'security',
185
+ severity: 'high',
186
+ confidence: 'likely',
187
+ title: 'SQL Injection via joins()',
188
+ description: 'Using string interpolation in joins() allows SQL injection.',
189
+ fix: { suggestion: 'Use symbol-based joins or sanitize input.' },
190
+ check({ files }) {
191
+ return checkRuby(this, files,
192
+ /\.joins\s*\(\s*["'].*#\{/,
193
+ );
194
+ },
195
+ },
196
+
197
+ // SEC-RUBY-010: SQL injection via calculate
198
+ {
199
+ id: 'SEC-RUBY-010',
200
+ category: 'security',
201
+ severity: 'high',
202
+ confidence: 'likely',
203
+ title: 'SQL Injection via calculate()',
204
+ description: 'Passing user-controlled strings to calculate() enables SQL injection.',
205
+ fix: { suggestion: 'Whitelist allowed aggregate operations and column names.' },
206
+ check({ files }) {
207
+ return checkRuby(this, files,
208
+ /\.calculate\s*\(\s*["'].*#\{/,
209
+ );
210
+ },
211
+ },
212
+
213
+ // ===========================================================================
214
+ // XSS (011–015)
215
+ // ===========================================================================
216
+
217
+ // SEC-RUBY-011: XSS via raw() helper
218
+ {
219
+ id: 'SEC-RUBY-011',
220
+ category: 'security',
221
+ severity: 'high',
222
+ confidence: 'likely',
223
+ title: 'XSS via raw() Helper',
224
+ description: 'Using raw() to render user input bypasses HTML escaping and enables XSS.',
225
+ fix: { suggestion: 'Use sanitize() or html_escape() instead of raw().' },
226
+ check({ files }) {
227
+ return checkRuby(this, files,
228
+ /\braw\s*\(/,
229
+ );
230
+ },
231
+ },
232
+
233
+ // SEC-RUBY-012: XSS via html_safe
234
+ {
235
+ id: 'SEC-RUBY-012',
236
+ category: 'security',
237
+ severity: 'high',
238
+ confidence: 'likely',
239
+ title: 'XSS via html_safe',
240
+ description: 'Calling html_safe on user-controlled strings disables HTML escaping.',
241
+ fix: { suggestion: 'Avoid html_safe on user input; use sanitize() instead.' },
242
+ check({ files }) {
243
+ return checkRuby(this, files,
244
+ /\.html_safe/,
245
+ );
246
+ },
247
+ },
248
+
249
+ // SEC-RUBY-013: XSS via content_tag with unsafe content
250
+ {
251
+ id: 'SEC-RUBY-013',
252
+ category: 'security',
253
+ severity: 'medium',
254
+ confidence: 'suggestion',
255
+ title: 'XSS via content_tag with Interpolation',
256
+ description: 'Using content_tag with interpolated user input may lead to XSS.',
257
+ fix: { suggestion: 'Ensure all content passed to content_tag is sanitized.' },
258
+ check({ files }) {
259
+ return checkRuby(this, files,
260
+ /content_tag\s*\(.*#\{/,
261
+ );
262
+ },
263
+ },
264
+
265
+ // SEC-RUBY-014: XSS via link_to with user-controlled href
266
+ {
267
+ id: 'SEC-RUBY-014',
268
+ category: 'security',
269
+ severity: 'medium',
270
+ confidence: 'suggestion',
271
+ title: 'XSS via link_to with User-Controlled URL',
272
+ description: 'Using link_to with user-controlled URLs can allow javascript: protocol XSS.',
273
+ fix: { suggestion: 'Validate URLs against an allowlist of protocols (http, https).' },
274
+ check({ files }) {
275
+ return checkRuby(this, files,
276
+ /link_to\s*.*params\[/,
277
+ );
278
+ },
279
+ },
280
+
281
+ // SEC-RUBY-015: XSS via render inline
282
+ {
283
+ id: 'SEC-RUBY-015',
284
+ category: 'security',
285
+ severity: 'high',
286
+ confidence: 'likely',
287
+ title: 'XSS via render inline',
288
+ description: 'Using render inline with user input allows arbitrary code execution and XSS.',
289
+ fix: { suggestion: 'Avoid render inline; use templates with proper escaping.' },
290
+ check({ files }) {
291
+ return checkRuby(this, files,
292
+ /render\s+inline\s*:/,
293
+ );
294
+ },
295
+ },
296
+
297
+ // ===========================================================================
298
+ // Mass Assignment (016–020)
299
+ // ===========================================================================
300
+
301
+ // SEC-RUBY-016: Mass assignment via permit!
302
+ {
303
+ id: 'SEC-RUBY-016',
304
+ category: 'security',
305
+ severity: 'high',
306
+ confidence: 'likely',
307
+ title: 'Mass Assignment via permit!',
308
+ description: 'Using permit! allows all parameters, bypassing strong parameters protection.',
309
+ fix: { suggestion: 'Explicitly list allowed parameters with permit(:field1, :field2).' },
310
+ check({ files }) {
311
+ return checkRuby(this, files,
312
+ /\.permit!/,
313
+ );
314
+ },
315
+ },
316
+
317
+ // SEC-RUBY-017: Mass assignment via attr_accessible bypass
318
+ {
319
+ id: 'SEC-RUBY-017',
320
+ category: 'security',
321
+ severity: 'high',
322
+ confidence: 'likely',
323
+ title: 'Unprotected Mass Assignment with attr_accessible',
324
+ description: 'Using without_protection: true bypasses attr_accessible whitelisting.',
325
+ fix: { suggestion: 'Remove without_protection option and properly whitelist attributes.' },
326
+ check({ files }) {
327
+ return checkRuby(this, files,
328
+ /without_protection\s*:\s*true/,
329
+ );
330
+ },
331
+ },
332
+
333
+ // SEC-RUBY-018: Mass assignment via update_attributes with params
334
+ {
335
+ id: 'SEC-RUBY-018',
336
+ category: 'security',
337
+ severity: 'high',
338
+ confidence: 'likely',
339
+ title: 'Mass Assignment via update_attributes with params',
340
+ description: 'Passing raw params to update_attributes allows mass assignment attacks.',
341
+ fix: { suggestion: 'Use strong parameters: update_attributes(user_params) with permit.' },
342
+ check({ files }) {
343
+ return checkRuby(this, files,
344
+ /update_attributes?\s*\(\s*params/,
345
+ );
346
+ },
347
+ },
348
+
349
+ // SEC-RUBY-019: Mass assignment via new/create with raw params
350
+ {
351
+ id: 'SEC-RUBY-019',
352
+ category: 'security',
353
+ severity: 'high',
354
+ confidence: 'likely',
355
+ title: 'Mass Assignment via new/create with Raw Params',
356
+ description: 'Passing raw params hash to new() or create() enables mass assignment.',
357
+ fix: { suggestion: 'Use strong parameters to whitelist allowed fields.' },
358
+ check({ files }) {
359
+ return checkRuby(this, files,
360
+ /\.(?:new|create|create!)\s*\(\s*params\b/,
361
+ );
362
+ },
363
+ },
364
+
365
+ // SEC-RUBY-020: Mass assignment via assign_attributes
366
+ {
367
+ id: 'SEC-RUBY-020',
368
+ category: 'security',
369
+ severity: 'high',
370
+ confidence: 'likely',
371
+ title: 'Mass Assignment via assign_attributes with Raw Params',
372
+ description: 'Passing raw params to assign_attributes allows mass assignment.',
373
+ fix: { suggestion: 'Filter params through strong parameters before assign_attributes.' },
374
+ check({ files }) {
375
+ return checkRuby(this, files,
376
+ /assign_attributes\s*\(\s*params/,
377
+ );
378
+ },
379
+ },
380
+
381
+ // ===========================================================================
382
+ // CSRF (021–025)
383
+ // ===========================================================================
384
+
385
+ // SEC-RUBY-021: CSRF protection disabled
386
+ {
387
+ id: 'SEC-RUBY-021',
388
+ category: 'security',
389
+ severity: 'high',
390
+ confidence: 'likely',
391
+ title: 'CSRF Protection Disabled',
392
+ description: 'skip_before_action :verify_authenticity_token disables CSRF protection.',
393
+ fix: { suggestion: 'Remove the skip_before_action or scope it to API-only actions with proper auth.' },
394
+ check({ files }) {
395
+ return checkRuby(this, files,
396
+ /skip_before_action\s+:verify_authenticity_token/,
397
+ );
398
+ },
399
+ },
400
+
401
+ // SEC-RUBY-022: CSRF protect_from_forgery with null_session
402
+ {
403
+ id: 'SEC-RUBY-022',
404
+ category: 'security',
405
+ severity: 'medium',
406
+ confidence: 'suggestion',
407
+ title: 'CSRF Protection with null_session',
408
+ description: 'protect_from_forgery with: :null_session may weaken CSRF protection.',
409
+ fix: { suggestion: 'Use protect_from_forgery with: :exception for web apps.' },
410
+ check({ files }) {
411
+ return checkRuby(this, files,
412
+ /protect_from_forgery\s+with:\s*:null_session/,
413
+ );
414
+ },
415
+ },
416
+
417
+ // SEC-RUBY-023: Missing CSRF protection
418
+ {
419
+ id: 'SEC-RUBY-023',
420
+ category: 'security',
421
+ severity: 'high',
422
+ confidence: 'suggestion',
423
+ title: 'Missing protect_from_forgery in ApplicationController',
424
+ description: 'ApplicationController without protect_from_forgery is vulnerable to CSRF.',
425
+ fix: { suggestion: 'Add protect_from_forgery with: :exception to ApplicationController.' },
426
+ check({ files }) {
427
+ const findings = [];
428
+ for (const [path, content] of files) {
429
+ if (SKIP_PATH.test(path)) continue;
430
+ if (isRuby(path)) {
431
+ if (/class\s+ApplicationController/.test(content) && /skip_forgery_protection/.test(content)) {
432
+ findings.push({ ruleId: this.id, file: path, line: 1, title: this.title, severity: this.severity });
433
+ }
434
+ }
435
+ }
436
+ return findings;
437
+ },
438
+ },
439
+
440
+ // SEC-RUBY-024: CSRF token in GET request
441
+ {
442
+ id: 'SEC-RUBY-024',
443
+ category: 'security',
444
+ severity: 'medium',
445
+ confidence: 'suggestion',
446
+ title: 'State-Changing Action via GET Route',
447
+ description: 'Using GET for state-changing actions exposes CSRF tokens in URLs and logs.',
448
+ fix: { suggestion: 'Use POST/PUT/PATCH/DELETE for state-changing operations.' },
449
+ check({ files }) {
450
+ return checkRuby(this, files,
451
+ /get\s+['"].*(?:delete|destroy|update|create|remove)/,
452
+ );
453
+ },
454
+ },
455
+
456
+ // SEC-RUBY-025: CSRF protection disabled globally
457
+ {
458
+ id: 'SEC-RUBY-025',
459
+ category: 'security',
460
+ severity: 'critical',
461
+ confidence: 'likely',
462
+ title: 'CSRF Protection Disabled via Configuration',
463
+ description: 'Setting allow_forgery_protection to false disables CSRF globally.',
464
+ fix: { suggestion: 'Set config.action_controller.allow_forgery_protection = true.' },
465
+ check({ files }) {
466
+ return checkRuby(this, files,
467
+ /allow_forgery_protection\s*=\s*false/,
468
+ );
469
+ },
470
+ },
471
+
472
+ // ===========================================================================
473
+ // Command Injection (026–030)
474
+ // ===========================================================================
475
+
476
+ // SEC-RUBY-026: Command injection via system()
477
+ {
478
+ id: 'SEC-RUBY-026',
479
+ category: 'security',
480
+ severity: 'critical',
481
+ confidence: 'likely',
482
+ title: 'Command Injection via system()',
483
+ description: 'Passing user input to system() allows command injection.',
484
+ fix: { suggestion: 'Use the array form: system("cmd", arg1, arg2) to avoid shell interpretation.' },
485
+ check({ files }) {
486
+ return checkRuby(this, files,
487
+ /\bsystem\s*\(\s*["'].*#\{/,
488
+ );
489
+ },
490
+ },
491
+
492
+ // SEC-RUBY-027: Command injection via backticks
493
+ {
494
+ id: 'SEC-RUBY-027',
495
+ category: 'security',
496
+ severity: 'critical',
497
+ confidence: 'likely',
498
+ title: 'Command Injection via Backticks',
499
+ description: 'Using backticks with interpolation allows command injection.',
500
+ fix: { suggestion: 'Use Open3.capture3 or system() array form instead of backticks.' },
501
+ check({ files }) {
502
+ return checkRuby(this, files,
503
+ /`[^`]*#\{/,
504
+ );
505
+ },
506
+ },
507
+
508
+ // SEC-RUBY-028: Command injection via exec()
509
+ {
510
+ id: 'SEC-RUBY-028',
511
+ category: 'security',
512
+ severity: 'critical',
513
+ confidence: 'likely',
514
+ title: 'Command Injection via exec()',
515
+ description: 'Passing user input to exec() allows command injection.',
516
+ fix: { suggestion: 'Use exec with array form: exec("cmd", arg1) to avoid shell interpretation.' },
517
+ check({ files }) {
518
+ return checkRuby(this, files,
519
+ /\bexec\s*\(\s*["'].*#\{/,
520
+ );
521
+ },
522
+ },
523
+
524
+ // SEC-RUBY-029: Command injection via IO.popen
525
+ {
526
+ id: 'SEC-RUBY-029',
527
+ category: 'security',
528
+ severity: 'critical',
529
+ confidence: 'likely',
530
+ title: 'Command Injection via IO.popen',
531
+ description: 'Using IO.popen with string interpolation allows command injection.',
532
+ fix: { suggestion: 'Use IO.popen with array form: IO.popen(["cmd", arg]).' },
533
+ check({ files }) {
534
+ return checkRuby(this, files,
535
+ /IO\.popen\s*\(\s*["'].*#\{/,
536
+ );
537
+ },
538
+ },
539
+
540
+ // SEC-RUBY-030: Command injection via Open3
541
+ {
542
+ id: 'SEC-RUBY-030',
543
+ category: 'security',
544
+ severity: 'critical',
545
+ confidence: 'likely',
546
+ title: 'Command Injection via Open3 with Interpolation',
547
+ description: 'Using Open3 methods with string interpolation allows command injection.',
548
+ fix: { suggestion: 'Use Open3 with array arguments to avoid shell interpretation.' },
549
+ check({ files }) {
550
+ return checkRuby(this, files,
551
+ /Open3\.(?:capture3|popen3|popen2)\s*\(\s*["'].*#\{/,
552
+ );
553
+ },
554
+ },
555
+
556
+ // ===========================================================================
557
+ // YAML/Marshal Deserialization (031–035)
558
+ // ===========================================================================
559
+
560
+ // SEC-RUBY-031: Unsafe YAML.load
561
+ {
562
+ id: 'SEC-RUBY-031',
563
+ category: 'security',
564
+ severity: 'critical',
565
+ confidence: 'likely',
566
+ title: 'Unsafe YAML.load',
567
+ description: 'YAML.load can deserialize arbitrary Ruby objects, leading to RCE.',
568
+ fix: { suggestion: 'Use YAML.safe_load instead of YAML.load.' },
569
+ check({ files }) {
570
+ return checkRuby(this, files,
571
+ /YAML\.load\s*\(/,
572
+ );
573
+ },
574
+ },
575
+
576
+ // SEC-RUBY-032: Marshal.load with untrusted data
577
+ {
578
+ id: 'SEC-RUBY-032',
579
+ category: 'security',
580
+ severity: 'critical',
581
+ confidence: 'likely',
582
+ title: 'Unsafe Marshal.load',
583
+ description: 'Marshal.load can execute arbitrary code when deserializing untrusted data.',
584
+ fix: { suggestion: 'Use JSON or YAML.safe_load instead of Marshal for untrusted data.' },
585
+ check({ files }) {
586
+ return checkRuby(this, files,
587
+ /Marshal\.load\s*\(/,
588
+ );
589
+ },
590
+ },
591
+
592
+ // SEC-RUBY-033: Marshal.restore with untrusted data
593
+ {
594
+ id: 'SEC-RUBY-033',
595
+ category: 'security',
596
+ severity: 'critical',
597
+ confidence: 'likely',
598
+ title: 'Unsafe Marshal.restore',
599
+ description: 'Marshal.restore is an alias for Marshal.load and is equally dangerous.',
600
+ fix: { suggestion: 'Use JSON or YAML.safe_load instead of Marshal.restore.' },
601
+ check({ files }) {
602
+ return checkRuby(this, files,
603
+ /Marshal\.restore\s*\(/,
604
+ );
605
+ },
606
+ },
607
+
608
+ // SEC-RUBY-034: YAML.load with permitted_classes bypass
609
+ {
610
+ id: 'SEC-RUBY-034',
611
+ category: 'security',
612
+ severity: 'high',
613
+ confidence: 'suggestion',
614
+ title: 'YAML.safe_load with Overly Broad permitted_classes',
615
+ description: 'Allowing too many classes in YAML.safe_load can still be exploitable.',
616
+ fix: { suggestion: 'Minimize permitted_classes to only those strictly needed.' },
617
+ check({ files }) {
618
+ return checkRuby(this, files,
619
+ /YAML\.safe_load\s*\(.*permitted_classes\s*:\s*\[.*Symbol/,
620
+ );
621
+ },
622
+ },
623
+
624
+ // SEC-RUBY-035: ERB.new with user input
625
+ {
626
+ id: 'SEC-RUBY-035',
627
+ category: 'security',
628
+ severity: 'critical',
629
+ confidence: 'likely',
630
+ title: 'Server-Side Template Injection via ERB.new',
631
+ description: 'Passing user input to ERB.new allows arbitrary code execution.',
632
+ fix: { suggestion: 'Never pass user input to ERB.new; use pre-defined templates.' },
633
+ check({ files }) {
634
+ return checkRuby(this, files,
635
+ /ERB\.new\s*\(.*params/,
636
+ );
637
+ },
638
+ },
639
+
640
+ // ===========================================================================
641
+ // File Access (036–040)
642
+ // ===========================================================================
643
+
644
+ // SEC-RUBY-036: Path traversal via File.read
645
+ {
646
+ id: 'SEC-RUBY-036',
647
+ category: 'security',
648
+ severity: 'high',
649
+ confidence: 'likely',
650
+ title: 'Path Traversal via File.read with User Input',
651
+ description: 'Passing user input to File.read allows reading arbitrary files.',
652
+ fix: { suggestion: 'Validate and sanitize file paths; use File.expand_path and check prefix.' },
653
+ check({ files }) {
654
+ return checkRuby(this, files,
655
+ /File\.read\s*\(.*params/,
656
+ );
657
+ },
658
+ },
659
+
660
+ // SEC-RUBY-037: Path traversal via File.open
661
+ {
662
+ id: 'SEC-RUBY-037',
663
+ category: 'security',
664
+ severity: 'high',
665
+ confidence: 'likely',
666
+ title: 'Path Traversal via File.open with User Input',
667
+ description: 'Passing user input to File.open allows reading or writing arbitrary files.',
668
+ fix: { suggestion: 'Validate file paths against a base directory allowlist.' },
669
+ check({ files }) {
670
+ return checkRuby(this, files,
671
+ /File\.open\s*\(.*params/,
672
+ );
673
+ },
674
+ },
675
+
676
+ // SEC-RUBY-038: send_file with user input
677
+ {
678
+ id: 'SEC-RUBY-038',
679
+ category: 'security',
680
+ severity: 'high',
681
+ confidence: 'likely',
682
+ title: 'Path Traversal via send_file',
683
+ description: 'Using send_file with user-controlled path allows arbitrary file disclosure.',
684
+ fix: { suggestion: 'Validate the file path and ensure it stays within the allowed directory.' },
685
+ check({ files }) {
686
+ return checkRuby(this, files,
687
+ /send_file\s*\(.*params/,
688
+ );
689
+ },
690
+ },
691
+
692
+ // SEC-RUBY-039: Tempfile with predictable name
693
+ {
694
+ id: 'SEC-RUBY-039',
695
+ category: 'security',
696
+ severity: 'medium',
697
+ confidence: 'suggestion',
698
+ title: 'Tempfile with Predictable Prefix',
699
+ description: 'Creating temporary files with predictable names may lead to symlink attacks.',
700
+ fix: { suggestion: 'Use SecureRandom for temp file names or let Tempfile handle naming.' },
701
+ check({ files }) {
702
+ return checkRuby(this, files,
703
+ /File\.(?:write|open)\s*\(\s*["']\/tmp\//,
704
+ );
705
+ },
706
+ },
707
+
708
+ // SEC-RUBY-040: File.delete with user input
709
+ {
710
+ id: 'SEC-RUBY-040',
711
+ category: 'security',
712
+ severity: 'high',
713
+ confidence: 'likely',
714
+ title: 'Arbitrary File Deletion',
715
+ description: 'Passing user input to File.delete or FileUtils.rm allows arbitrary file deletion.',
716
+ fix: { suggestion: 'Validate file paths and restrict to allowed directories.' },
717
+ check({ files }) {
718
+ return checkRuby(this, files,
719
+ /(?:File\.delete|FileUtils\.rm)\s*\(.*params/,
720
+ );
721
+ },
722
+ },
723
+
724
+ // ===========================================================================
725
+ // Metaprogramming (041–045)
726
+ // ===========================================================================
727
+
728
+ // SEC-RUBY-041: eval with user input
729
+ {
730
+ id: 'SEC-RUBY-041',
731
+ category: 'security',
732
+ severity: 'critical',
733
+ confidence: 'likely',
734
+ title: 'Code Injection via eval()',
735
+ description: 'Using eval with user-controlled input allows arbitrary code execution.',
736
+ fix: { suggestion: 'Avoid eval entirely; use safe alternatives like case/when dispatching.' },
737
+ check({ files }) {
738
+ return checkRuby(this, files,
739
+ /\beval\s*\(\s*["'].*#\{/,
740
+ );
741
+ },
742
+ },
743
+
744
+ // SEC-RUBY-042: send with user input
745
+ {
746
+ id: 'SEC-RUBY-042',
747
+ category: 'security',
748
+ severity: 'high',
749
+ confidence: 'likely',
750
+ title: 'Unsafe Dynamic Dispatch via send()',
751
+ description: 'Using send() with user-controlled method names allows calling any method.',
752
+ fix: { suggestion: 'Whitelist allowed method names before using send().' },
753
+ check({ files }) {
754
+ return checkRuby(this, files,
755
+ /\.send\s*\(\s*params/,
756
+ );
757
+ },
758
+ },
759
+
760
+ // SEC-RUBY-043: constantize with user input
761
+ {
762
+ id: 'SEC-RUBY-043',
763
+ category: 'security',
764
+ severity: 'critical',
765
+ confidence: 'likely',
766
+ title: 'Unsafe constantize with User Input',
767
+ description: 'Using constantize on user input allows instantiation of arbitrary classes.',
768
+ fix: { suggestion: 'Whitelist allowed class names before calling constantize.' },
769
+ check({ files }) {
770
+ return checkRuby(this, files,
771
+ /params\[.*\]\.constantize/,
772
+ );
773
+ },
774
+ },
775
+
776
+ // SEC-RUBY-044: class_eval with user input
777
+ {
778
+ id: 'SEC-RUBY-044',
779
+ category: 'security',
780
+ severity: 'critical',
781
+ confidence: 'likely',
782
+ title: 'Code Injection via class_eval',
783
+ description: 'Using class_eval with interpolated strings allows arbitrary code execution.',
784
+ fix: { suggestion: 'Use define_method or block form of class_eval instead of string eval.' },
785
+ check({ files }) {
786
+ return checkRuby(this, files,
787
+ /class_eval\s*\(\s*["'].*#\{/,
788
+ );
789
+ },
790
+ },
791
+
792
+ // SEC-RUBY-045: instance_variable_set with user input
793
+ {
794
+ id: 'SEC-RUBY-045',
795
+ category: 'security',
796
+ severity: 'high',
797
+ confidence: 'likely',
798
+ title: 'Unsafe instance_variable_set with User Input',
799
+ description: 'Using instance_variable_set with user-controlled names allows manipulation of internal state.',
800
+ fix: { suggestion: 'Whitelist allowed variable names before using instance_variable_set.' },
801
+ check({ files }) {
802
+ return checkRuby(this, files,
803
+ /instance_variable_set\s*\(.*params/,
804
+ );
805
+ },
806
+ },
807
+
808
+ // ===========================================================================
809
+ // Crypto (046–050)
810
+ // ===========================================================================
811
+
812
+ // SEC-RUBY-046: Use of MD5
813
+ {
814
+ id: 'SEC-RUBY-046',
815
+ category: 'security',
816
+ severity: 'high',
817
+ confidence: 'likely',
818
+ title: 'Weak Hash: MD5',
819
+ description: 'MD5 is cryptographically broken and should not be used for security purposes.',
820
+ fix: { suggestion: 'Use SHA-256 or SHA-3 via OpenSSL::Digest::SHA256.' },
821
+ check({ files }) {
822
+ return checkRuby(this, files,
823
+ /Digest::MD5/,
824
+ );
825
+ },
826
+ },
827
+
828
+ // SEC-RUBY-047: Use of SHA1
829
+ {
830
+ id: 'SEC-RUBY-047',
831
+ category: 'security',
832
+ severity: 'medium',
833
+ confidence: 'likely',
834
+ title: 'Weak Hash: SHA1',
835
+ description: 'SHA1 is considered weak for cryptographic purposes.',
836
+ fix: { suggestion: 'Use SHA-256 or SHA-3 instead of SHA1.' },
837
+ check({ files }) {
838
+ return checkRuby(this, files,
839
+ /Digest::SHA1/,
840
+ );
841
+ },
842
+ },
843
+
844
+ // SEC-RUBY-048: Hardcoded secret key
845
+ {
846
+ id: 'SEC-RUBY-048',
847
+ category: 'security',
848
+ severity: 'critical',
849
+ confidence: 'likely',
850
+ title: 'Hardcoded Secret Key',
851
+ description: 'Hardcoded secret_key_base exposes application to session forgery.',
852
+ fix: { suggestion: 'Use environment variables or credentials file for secret_key_base.' },
853
+ check({ files }) {
854
+ return checkRuby(this, files,
855
+ /secret_key_base\s*=\s*["'][a-f0-9]{30,}/,
856
+ );
857
+ },
858
+ },
859
+
860
+ // SEC-RUBY-049: Insecure random number generation
861
+ {
862
+ id: 'SEC-RUBY-049',
863
+ category: 'security',
864
+ severity: 'medium',
865
+ confidence: 'likely',
866
+ title: 'Insecure Random Number Generation',
867
+ description: 'Using rand() instead of SecureRandom for security-sensitive values.',
868
+ fix: { suggestion: 'Use SecureRandom.hex or SecureRandom.uuid for tokens and secrets.' },
869
+ check({ files }) {
870
+ return checkRuby(this, files,
871
+ /(?:token|secret|key|password|nonce)\s*=\s*rand\b/,
872
+ );
873
+ },
874
+ },
875
+
876
+ // SEC-RUBY-050: Weak cipher mode (ECB)
877
+ {
878
+ id: 'SEC-RUBY-050',
879
+ category: 'security',
880
+ severity: 'high',
881
+ confidence: 'likely',
882
+ title: 'Weak Cipher Mode: ECB',
883
+ description: 'ECB mode does not provide semantic security and leaks data patterns.',
884
+ fix: { suggestion: 'Use AES-256-GCM or AES-256-CBC with HMAC instead of ECB.' },
885
+ check({ files }) {
886
+ return checkRuby(this, files,
887
+ /OpenSSL::Cipher.*ECB/,
888
+ );
889
+ },
890
+ },
891
+
892
+ // ===========================================================================
893
+ // Session / Auth (051–055)
894
+ // ===========================================================================
895
+
896
+ // SEC-RUBY-051: Session stored in cookie without encryption
897
+ {
898
+ id: 'SEC-RUBY-051',
899
+ category: 'security',
900
+ severity: 'high',
901
+ confidence: 'likely',
902
+ title: 'Unencrypted Cookie Session Store',
903
+ description: 'Using CookieStore without encryption exposes session data to clients.',
904
+ fix: { suggestion: 'Use encrypted cookie store or database-backed sessions.' },
905
+ check({ files }) {
906
+ return checkRuby(this, files,
907
+ /session_store\s*:cookie_store/,
908
+ );
909
+ },
910
+ },
911
+
912
+ // SEC-RUBY-052: Devise with insecure configuration
913
+ {
914
+ id: 'SEC-RUBY-052',
915
+ category: 'security',
916
+ severity: 'high',
917
+ confidence: 'likely',
918
+ title: 'Devise Insecure stretches Configuration',
919
+ description: 'Setting Devise stretches too low weakens password hashing.',
920
+ fix: { suggestion: 'Set config.stretches to at least 12 in production.' },
921
+ check({ files }) {
922
+ return checkRuby(this, files,
923
+ /config\.stretches\s*=\s*[0-9]\b/,
924
+ );
925
+ },
926
+ },
927
+
928
+ // SEC-RUBY-053: Authentication bypass via skip_before_action
929
+ {
930
+ id: 'SEC-RUBY-053',
931
+ category: 'security',
932
+ severity: 'high',
933
+ confidence: 'suggestion',
934
+ title: 'Authentication Bypass via skip_before_action',
935
+ description: 'Skipping authenticate_user! may expose actions to unauthenticated users.',
936
+ fix: { suggestion: 'Carefully scope skip_before_action and verify authorization separately.' },
937
+ check({ files }) {
938
+ return checkRuby(this, files,
939
+ /skip_before_action\s+:authenticate/,
940
+ );
941
+ },
942
+ },
943
+
944
+ // SEC-RUBY-054: Insecure password comparison
945
+ {
946
+ id: 'SEC-RUBY-054',
947
+ category: 'security',
948
+ severity: 'high',
949
+ confidence: 'likely',
950
+ title: 'Insecure Password Comparison',
951
+ description: 'Using == to compare passwords is vulnerable to timing attacks.',
952
+ fix: { suggestion: 'Use ActiveSupport::SecurityUtils.secure_compare or bcrypt.' },
953
+ check({ files }) {
954
+ return checkRuby(this, files,
955
+ /password\s*==\s*/,
956
+ );
957
+ },
958
+ },
959
+
960
+ // SEC-RUBY-055: Hardcoded password in source
961
+ {
962
+ id: 'SEC-RUBY-055',
963
+ category: 'security',
964
+ severity: 'critical',
965
+ confidence: 'likely',
966
+ title: 'Hardcoded Password',
967
+ description: 'Hardcoded passwords in source code can be extracted by attackers.',
968
+ fix: { suggestion: 'Use environment variables or a secrets manager for passwords.' },
969
+ check({ files }) {
970
+ return checkRuby(this, files,
971
+ /(?:password|passwd)\s*=\s*["'][^"']{4,}["']/,
972
+ );
973
+ },
974
+ },
975
+
976
+ // ===========================================================================
977
+ // ERB / Slim Templates (056–060)
978
+ // ===========================================================================
979
+
980
+ // SEC-RUBY-056: Unescaped ERB output
981
+ {
982
+ id: 'SEC-RUBY-056',
983
+ category: 'security',
984
+ severity: 'high',
985
+ confidence: 'likely',
986
+ title: 'Unescaped ERB Output Tag',
987
+ description: 'Using <%== or raw in ERB templates outputs unescaped HTML, enabling XSS.',
988
+ fix: { suggestion: 'Use <%= which auto-escapes output, or sanitize() for HTML content.' },
989
+ check({ files }) {
990
+ return checkRuby(this, files,
991
+ /<%==\s/,
992
+ );
993
+ },
994
+ },
995
+
996
+ // SEC-RUBY-057: ERB template with direct params access
997
+ {
998
+ id: 'SEC-RUBY-057',
999
+ category: 'security',
1000
+ severity: 'high',
1001
+ confidence: 'likely',
1002
+ title: 'Direct params Access in Template',
1003
+ description: 'Accessing params directly in templates bypasses controller-level sanitization.',
1004
+ fix: { suggestion: 'Pass sanitized data from the controller via instance variables.' },
1005
+ check({ files }) {
1006
+ return checkRuby(this, files,
1007
+ /<%=.*params\[/,
1008
+ );
1009
+ },
1010
+ },
1011
+
1012
+ // SEC-RUBY-058: Slim template with unescaped output
1013
+ {
1014
+ id: 'SEC-RUBY-058',
1015
+ category: 'security',
1016
+ severity: 'high',
1017
+ confidence: 'likely',
1018
+ title: 'Unescaped Slim Output',
1019
+ description: 'Using == in Slim templates outputs unescaped HTML, enabling XSS.',
1020
+ fix: { suggestion: 'Use = for escaped output in Slim templates.' },
1021
+ check({ files }) {
1022
+ return checkRuby(this, files,
1023
+ /^\s*==\s+.*params/,
1024
+ );
1025
+ },
1026
+ },
1027
+
1028
+ // SEC-RUBY-059: render with user-controlled template
1029
+ {
1030
+ id: 'SEC-RUBY-059',
1031
+ category: 'security',
1032
+ severity: 'critical',
1033
+ confidence: 'likely',
1034
+ title: 'Template Injection via render with User Input',
1035
+ description: 'Passing user input to render() template name allows arbitrary template rendering.',
1036
+ fix: { suggestion: 'Whitelist allowed template names; never pass params directly to render.' },
1037
+ check({ files }) {
1038
+ return checkRuby(this, files,
1039
+ /render\s+params\[/,
1040
+ );
1041
+ },
1042
+ },
1043
+
1044
+ // SEC-RUBY-060: Haml unescaped output
1045
+ {
1046
+ id: 'SEC-RUBY-060',
1047
+ category: 'security',
1048
+ severity: 'high',
1049
+ confidence: 'likely',
1050
+ title: 'Unescaped Haml Output',
1051
+ description: 'Using != in Haml templates outputs unescaped HTML, enabling XSS.',
1052
+ fix: { suggestion: 'Use = for auto-escaped output in Haml templates.' },
1053
+ check({ files }) {
1054
+ return checkRuby(this, files,
1055
+ /!=\s+.*params/,
1056
+ );
1057
+ },
1058
+ },
1059
+ ];
1060
+
1061
+ export default rules;