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,608 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Ruby & PHP auto-fix transforms (70 total: 35 Ruby + 35 PHP)
3
+ // ---------------------------------------------------------------------------
4
+ // Each fix function takes a content string and returns the modified string,
5
+ // or null if the fix could not be applied.
6
+ //
7
+ // R1-R10 and P1-P10 are re-exported from fixer.js so that this module is the
8
+ // single source of truth for all Ruby and PHP fixes.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import {
12
+ fixRubyFindBySql,
13
+ fixRubyHtmlSafe,
14
+ fixRubySystemCall,
15
+ fixRubyYamlLoad,
16
+ fixRubyMarshalLoad,
17
+ fixRubyPermitAll,
18
+ fixRubySkipCsrf,
19
+ fixRubyEval as fixRubyEvalInput,
20
+ fixRubyOpenUri,
21
+ fixRubyWeakHash,
22
+ fixPhpMysqlQuery,
23
+ fixPhpEcho as fixPhpEchoUnsafe,
24
+ fixPhpEval,
25
+ fixPhpExec as fixPhpExecInput,
26
+ fixPhpMd5Password,
27
+ fixPhpExtract as fixPhpExtractPost,
28
+ fixPhpLooseComparison,
29
+ fixPhpSessionConfig,
30
+ fixPhpUnserialize,
31
+ fixPhpInclude as fixPhpIncludeInput,
32
+ } from './fixer.js';
33
+
34
+ // Re-export the imported fixes so consumers get everything from this module
35
+ export {
36
+ fixRubyFindBySql,
37
+ fixRubyHtmlSafe,
38
+ fixRubySystemCall,
39
+ fixRubyYamlLoad,
40
+ fixRubyMarshalLoad,
41
+ fixRubyPermitAll,
42
+ fixRubySkipCsrf,
43
+ fixRubyEvalInput,
44
+ fixRubyOpenUri,
45
+ fixRubyWeakHash,
46
+ fixPhpMysqlQuery,
47
+ fixPhpEchoUnsafe,
48
+ fixPhpEval,
49
+ fixPhpExecInput,
50
+ fixPhpMd5Password,
51
+ fixPhpExtractPost,
52
+ fixPhpLooseComparison,
53
+ fixPhpSessionConfig,
54
+ fixPhpUnserialize,
55
+ fixPhpIncludeInput,
56
+ };
57
+
58
+ // ===========================================================================
59
+ // Ruby fixes (R11 – R35)
60
+ // ===========================================================================
61
+
62
+ // R11. Replace hardcoded secret with ENV lookup
63
+ export function fixRubyHardcodedSecret(content) {
64
+ const modified = content.replace(
65
+ /(\w*secret\w*)\s*=\s*["']([^"']{8,})["']/gi,
66
+ "$1 = ENV['SECRET'] # TODO: set SECRET in environment",
67
+ );
68
+ return modified === content ? null : modified;
69
+ }
70
+
71
+ // R12. Add strong params wrapper where params used directly
72
+ export function fixRubyNoStrongParams(content) {
73
+ const modified = content.replace(
74
+ /(\w+)\.create\(\s*params\s*\)/g,
75
+ "$1.create(permitted_params) # TODO: define permitted_params via params.require(...).permit(...)",
76
+ );
77
+ return modified === content ? null : modified;
78
+ }
79
+
80
+ // R13. Replace raw output with h() helper
81
+ export function fixRubyRawOutput(content) {
82
+ const modified = content.replace(
83
+ /<%=\s*raw\s+(\w+)\s*%>/g,
84
+ '<%= h($1) %>',
85
+ );
86
+ return modified === content ? null : modified;
87
+ }
88
+
89
+ // R14. Add Shellwords.shellescape to exec input
90
+ export function fixRubyExecInput(content) {
91
+ const modified = content.replace(
92
+ /`([^`]*?)#\{(\w+)\}([^`]*?)`/g,
93
+ '`$1#{Shellwords.shellescape($2)}$3`',
94
+ );
95
+ return modified === content ? null : modified;
96
+ }
97
+
98
+ // R15. Replace backticks command with Open3.capture3
99
+ export function fixRubyBackticksInput(content) {
100
+ const modified = content.replace(
101
+ /(\w+)\s*=\s*`([^`]+)`/g,
102
+ '$1_out, $1_err, $1_status = Open3.capture3("$2")',
103
+ );
104
+ return modified === content ? null : modified;
105
+ }
106
+
107
+ // R16. Replace send() with public_send()
108
+ export function fixRubySendUnsafe(content) {
109
+ const modified = content.replace(
110
+ /\.send\(/g,
111
+ '.public_send(',
112
+ );
113
+ return modified === content ? null : modified;
114
+ }
115
+
116
+ // R17. Add whitelist check to const_get
117
+ export function fixRubyConstGet(content) {
118
+ const modified = content.replace(
119
+ /const_get\((\w+)\)/g,
120
+ 'const_get($1) # SECURITY: validate $1 against an allowlist before calling const_get',
121
+ );
122
+ return modified === content ? null : modified;
123
+ }
124
+
125
+ // R18. Add timeout to HTTP requests
126
+ export function fixRubyNoTimeout(content) {
127
+ const modified = content.replace(
128
+ /Net::HTTP\.get\(([^)]+)\)/g,
129
+ 'Net::HTTP.start($1, open_timeout: 10, read_timeout: 10) { |http| http.get($1) }',
130
+ );
131
+ return modified === content ? null : modified;
132
+ }
133
+
134
+ // R19. Replace VERIFY_NONE with VERIFY_PEER
135
+ export function fixRubyInsecureSsl(content) {
136
+ const modified = content.replace(
137
+ /OpenSSL::SSL::VERIFY_NONE/g,
138
+ 'OpenSSL::SSL::VERIFY_PEER',
139
+ );
140
+ return modified === content ? null : modified;
141
+ }
142
+
143
+ // R20. Add Rails.logger where puts used in controllers
144
+ export function fixRubyNoLogging(content) {
145
+ if (!/class\s+\w+Controller/.test(content)) return null;
146
+ const modified = content.replace(
147
+ /\bputs\s+/g,
148
+ 'Rails.logger.info ',
149
+ );
150
+ return modified === content ? null : modified;
151
+ }
152
+
153
+ // R21. Replace hardcoded DB credentials with database.yml reference
154
+ export function fixRubyHardcodedDb(content) {
155
+ const modified = content.replace(
156
+ /((?:password|passwd|db_pass)\s*[:=]\s*)["']([^"']+)["']/gi,
157
+ "$1ENV['DATABASE_PASSWORD'] # TODO: move credential to database.yml / Rails credentials",
158
+ );
159
+ return modified === content ? null : modified;
160
+ }
161
+
162
+ // R22. Replace hardcoded session secret with credentials.yml
163
+ export function fixRubySessionSecret(content) {
164
+ const modified = content.replace(
165
+ /(secret_key_base\s*=\s*)["']([^"']+)["']/g,
166
+ "$1Rails.application.credentials.secret_key_base",
167
+ );
168
+ return modified === content ? null : modified;
169
+ }
170
+
171
+ // R23. Add rack-attack rate limiting comment
172
+ export function fixRubyNoRateLimit(content) {
173
+ if (!/class\s+\w+Controller/.test(content)) return null;
174
+ if (/rack.attack|throttle|rate.limit/i.test(content)) return null;
175
+ const modified = content.replace(
176
+ /(class\s+\w+Controller\s*<\s*\w+)/,
177
+ "# TODO: add Rack::Attack throttle rules for rate limiting\n$1",
178
+ );
179
+ return modified === content ? null : modified;
180
+ }
181
+
182
+ // R24. Sanitize path in File.read
183
+ export function fixRubyFileReadInput(content) {
184
+ const modified = content.replace(
185
+ /File\.read\((\w+)\)/g,
186
+ 'File.read(File.expand_path($1, Rails.root.join("safe_dir"))) # SECURITY: validate path',
187
+ );
188
+ return modified === content ? null : modified;
189
+ }
190
+
191
+ // R25. Escape glob pattern input
192
+ export function fixRubyGlobInput(content) {
193
+ const modified = content.replace(
194
+ /Dir\.glob\((\w+)\)/g,
195
+ 'Dir.glob($1.gsub(/[\\[\\]\\*\\?]/, "")) # SECURITY: sanitize glob input',
196
+ );
197
+ return modified === content ? null : modified;
198
+ }
199
+
200
+ // R26. Wrap user input in Regexp.escape
201
+ export function fixRubyRegexpInput(content) {
202
+ const modified = content.replace(
203
+ /Regexp\.new\((\w+)\)/g,
204
+ 'Regexp.new(Regexp.escape($1))',
205
+ );
206
+ return modified === content ? null : modified;
207
+ }
208
+
209
+ // R27. Add before_action :authenticate_user! where missing
210
+ export function fixRubyNoAuth(content) {
211
+ if (!/class\s+\w+Controller/.test(content)) return null;
212
+ if (/before_action\s+:authenticate|skip_before_action\s+:authenticate/.test(content)) return null;
213
+ const modified = content.replace(
214
+ /(class\s+\w+Controller\s*<\s*\w+)/,
215
+ "$1\n before_action :authenticate_user! # TODO: verify authentication requirement",
216
+ );
217
+ return modified === content ? null : modified;
218
+ }
219
+
220
+ // R28. Replace render inline: with template rendering
221
+ export function fixRubyRenderInline(content) {
222
+ const modified = content.replace(
223
+ /render\s+inline:\s*["']([^"']+)["']/g,
224
+ 'render template: "shared/inline_template" # SECURITY: replaced inline render — move markup to a template',
225
+ );
226
+ return modified === content ? null : modified;
227
+ }
228
+
229
+ // R29. Add URL whitelist to redirect
230
+ export function fixRubyRedirectInput(content) {
231
+ const modified = content.replace(
232
+ /redirect_to\s+(\w+)\b(?!\s*_path|\s*_url)/g,
233
+ 'redirect_to($1 =~ /\\A\\// ? $1 : root_path) # SECURITY: validate redirect target',
234
+ );
235
+ return modified === content ? null : modified;
236
+ }
237
+
238
+ // R30. Replace ENV direct access with Rails.credentials
239
+ export function fixRubyNoEncryptedCreds(content) {
240
+ const modified = content.replace(
241
+ /ENV\[['"](\w*(?:SECRET|KEY|TOKEN)\w*)['"]\]/g,
242
+ 'Rails.application.credentials.$1 # TODO: add to credentials.yml.enc',
243
+ );
244
+ return modified === content ? null : modified;
245
+ }
246
+
247
+ // R31. Replace deprecated find(:all) with where().first
248
+ export function fixRubyDeprecatedFind(content) {
249
+ const modified = content.replace(
250
+ /\.find\(:all,\s*conditions:\s*\[([^\]]+)\]\)/g,
251
+ '.where($1)',
252
+ );
253
+ return modified === content ? null : modified;
254
+ }
255
+
256
+ // R32. Replace mass assignment with explicit attributes
257
+ export function fixRubyMassAssignment(content) {
258
+ const modified = content.replace(
259
+ /\.update_attributes\(params\[:\w+\]\)/g,
260
+ '.update(permitted_params) # TODO: use strong parameters — params.require(:model).permit(:field1, :field2)',
261
+ );
262
+ return modified === content ? null : modified;
263
+ }
264
+
265
+ // R33. Add validates presence to model
266
+ export function fixRubyNoValidation(content) {
267
+ if (!/class\s+\w+\s*<\s*(?:ActiveRecord::Base|ApplicationRecord)/.test(content)) return null;
268
+ if (/validates\s/.test(content)) return null;
269
+ const modified = content.replace(
270
+ /(class\s+\w+\s*<\s*(?:ActiveRecord::Base|ApplicationRecord))/,
271
+ "$1\n # TODO: add model validations\n # validates :name, presence: true",
272
+ );
273
+ return modified === content ? null : modified;
274
+ }
275
+
276
+ // R34. Replace hardcoded email with config setting
277
+ export function fixRubyHardcodedEmail(content) {
278
+ const modified = content.replace(
279
+ /((?:from|to|email)\s*[:=]\s*)["'](\w+@\w+\.\w+)["']/gi,
280
+ "$1Rails.configuration.default_email # TODO: configure email in application.rb",
281
+ );
282
+ return modified === content ? null : modified;
283
+ }
284
+
285
+ // R35. Add rescue_from for error handling
286
+ export function fixRubyNoErrorHandler(content) {
287
+ if (!/class\s+\w+Controller/.test(content)) return null;
288
+ if (/rescue_from/.test(content)) return null;
289
+ const modified = content.replace(
290
+ /(class\s+(\w+Controller)\s*<\s*\w+)/,
291
+ "$1\n rescue_from StandardError, with: :handle_error\n\n private\n\n def handle_error(e)\n Rails.logger.error(e.message)\n render json: { error: 'Internal server error' }, status: :internal_server_error\n end",
292
+ );
293
+ return modified === content ? null : modified;
294
+ }
295
+
296
+ // ===========================================================================
297
+ // PHP fixes (P11 – P35)
298
+ // ===========================================================================
299
+
300
+ // P11. Add escapeshellcmd to system() calls
301
+ export function fixPhpSystemCall(content) {
302
+ const modified = content.replace(
303
+ /system\(\s*\$(\w+)\s*\)/g,
304
+ 'system(escapeshellcmd($$1))',
305
+ );
306
+ return modified === content ? null : modified;
307
+ }
308
+
309
+ // P12. Replace passthru with proc_open
310
+ export function fixPhpPassthru(content) {
311
+ const modified = content.replace(
312
+ /passthru\(\s*\$(\w+)\s*\)/g,
313
+ '/* SECURITY: replaced passthru with proc_open */\n$proc = proc_open(escapeshellcmd($$1), [1 => ["pipe", "w"]], $pipes); echo stream_get_contents($pipes[1]); proc_close($proc)',
314
+ );
315
+ return modified === content ? null : modified;
316
+ }
317
+
318
+ // P13. Replace preg_replace with /e modifier with preg_replace_callback
319
+ export function fixPhpPreg_e(content) {
320
+ const modified = content.replace(
321
+ /preg_replace\(\s*(['"])([^'"]*?)e(['"]),\s*(['"][^'"]*?['"])/g,
322
+ 'preg_replace_callback($1$2$3, function($m) { return $4; }',
323
+ );
324
+ return modified === content ? null : modified;
325
+ }
326
+
327
+ // P14. Replace global variable with function parameter
328
+ export function fixPhpGlobalVar(content) {
329
+ const modified = content.replace(
330
+ /\bglobal\s+\$(\w+);/g,
331
+ '/* SECURITY: avoid global — pass $$1 as a function parameter instead */',
332
+ );
333
+ return modified === content ? null : modified;
334
+ }
335
+
336
+ // P15. Replace magic_quotes reliance with proper escaping
337
+ export function fixPhpMagicQuotes(content) {
338
+ const modified = content.replace(
339
+ /get_magic_quotes_gpc\(\)/g,
340
+ 'false /* magic_quotes removed in PHP 7.4 — use prepared statements or proper escaping */',
341
+ );
342
+ return modified === content ? null : modified;
343
+ }
344
+
345
+ // P16. Replace file_get_contents with curl + validation
346
+ export function fixPhpFileGetContents(content) {
347
+ const modified = content.replace(
348
+ /file_get_contents\(\s*\$(\w+)\s*\)/g,
349
+ '/* SECURITY: validate URL before fetching */ (function() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, filter_var($$1, FILTER_VALIDATE_URL)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); $r = curl_exec($ch); curl_close($ch); return $r; })()',
350
+ );
351
+ return modified === content ? null : modified;
352
+ }
353
+
354
+ // P17. Replace hardcoded DB credentials with env variable
355
+ export function fixPhpHardcodedDb(content) {
356
+ const modified = content.replace(
357
+ /((?:password|passwd|db_pass)\s*=\s*)['"]([^'"]+)['"]/gi,
358
+ "$1getenv('DB_PASSWORD') /* TODO: set DB_PASSWORD in .env */",
359
+ );
360
+ return modified === content ? null : modified;
361
+ }
362
+
363
+ // P18. Add rate-limit middleware comment
364
+ export function fixPhpNoRateLimit(content) {
365
+ if (/throttle|rate.limit|RateLimiter/i.test(content)) return null;
366
+ if (!/Route::/.test(content)) return null;
367
+ const modified = content.replace(
368
+ /(Route::\w+\([^)]+\))/,
369
+ "$1->middleware('throttle:60,1') /* TODO: configure rate limiting */",
370
+ );
371
+ return modified === content ? null : modified;
372
+ }
373
+
374
+ // P19. Add filter_var to unvalidated input
375
+ export function fixPhpNoInputFilter(content) {
376
+ const modified = content.replace(
377
+ /\$_GET\[['"](\w+)['"]\]/g,
378
+ "filter_input(INPUT_GET, '$1', FILTER_SANITIZE_SPECIAL_CHARS)",
379
+ );
380
+ return modified === content ? null : modified;
381
+ }
382
+
383
+ // P20. Replace rand() with random_bytes()
384
+ export function fixPhpWeakRandom(content) {
385
+ const modified = content.replace(
386
+ /\brand\(\s*\)/g,
387
+ 'random_int(0, PHP_INT_MAX) /* SECURITY: use cryptographically secure random */',
388
+ );
389
+ return modified === content ? null : modified;
390
+ }
391
+
392
+ // P21. Add type hints to function parameters
393
+ export function fixPhpNoTypeHints(content) {
394
+ const modified = content.replace(
395
+ /function\s+(\w+)\(\s*\$(\w+)\s*\)/g,
396
+ 'function $1(mixed $$2) /* TODO: replace mixed with proper type hint */',
397
+ );
398
+ return modified === content ? null : modified;
399
+ }
400
+
401
+ // P22. Add CSRF token to form
402
+ export function fixPhpNoCsrf(content) {
403
+ const modified = content.replace(
404
+ /(<form\s[^>]*method\s*=\s*["']post["'][^>]*>)/gi,
405
+ '$1\n <?php echo csrf_token(); ?> <!-- SECURITY: add CSRF protection -->',
406
+ );
407
+ return modified === content ? null : modified;
408
+ }
409
+
410
+ // P23. Replace {!! !!} with {{ }} in Blade templates
411
+ export function fixPhpBladUnescaped(content) {
412
+ const modified = content.replace(
413
+ /\{!!\s*(\$\w+)\s*!!\}/g,
414
+ '{{ $1 }}',
415
+ );
416
+ return modified === content ? null : modified;
417
+ }
418
+
419
+ // P24. Replace DB::raw with Eloquent query builder
420
+ export function fixPhpLaravelRawQuery(content) {
421
+ const modified = content.replace(
422
+ /DB::select\(\s*DB::raw\(\s*"([^"]*?)\{\$(\w+)\}([^"]*?)"\s*\)\s*\)/g,
423
+ 'DB::select("$1?$3", [$$2])',
424
+ );
425
+ return modified === content ? null : modified;
426
+ }
427
+
428
+ // P25. Add validate() call to controller method
429
+ export function fixPhpNoValidation(content) {
430
+ const modified = content.replace(
431
+ /(\$request->(?:input|all)\(\))/g,
432
+ '$request->validate([/* TODO: add validation rules */]) /* SECURITY: validate input */',
433
+ );
434
+ return modified === content ? null : modified;
435
+ }
436
+
437
+ // P26. Remove phpinfo() calls
438
+ export function fixPhpEnvExposure(content) {
439
+ const modified = content.replace(
440
+ /\bphpinfo\(\s*\)\s*;?/g,
441
+ '/* SECURITY: phpinfo() removed — exposes server configuration */',
442
+ );
443
+ return modified === content ? null : modified;
444
+ }
445
+
446
+ // P27. Set APP_DEBUG to false
447
+ export function fixPhpDebugMode(content) {
448
+ const modified = content.replace(
449
+ /APP_DEBUG\s*=\s*true/gi,
450
+ "APP_DEBUG=false /* SECURITY: disable debug mode in production */",
451
+ );
452
+ return modified === content ? null : modified;
453
+ }
454
+
455
+ // P28. Add open_basedir directive
456
+ export function fixPhpOpenBasedir(content) {
457
+ if (/open_basedir/.test(content)) return null;
458
+ const modified = content.replace(
459
+ /(<\?php)/,
460
+ '$1\nini_set("open_basedir", __DIR__); /* SECURITY: restrict file access to application directory */',
461
+ );
462
+ return modified === content ? null : modified;
463
+ }
464
+
465
+ // P29. Disable allow_url_include
466
+ export function fixPhpAllowUrlInclude(content) {
467
+ const modified = content.replace(
468
+ /allow_url_include\s*=\s*(?:On|1|true)/gi,
469
+ 'allow_url_include = Off ; SECURITY: remote file inclusion disabled',
470
+ );
471
+ return modified === content ? null : modified;
472
+ }
473
+
474
+ // P30. Disable display_errors
475
+ export function fixPhpNoErrorDisplay(content) {
476
+ const modified = content.replace(
477
+ /display_errors\s*=\s*(?:On|1|true)/gi,
478
+ 'display_errors = Off ; SECURITY: do not expose errors to users',
479
+ );
480
+ return modified === content ? null : modified;
481
+ }
482
+
483
+ // P31. Replace hardcoded secret with getenv()
484
+ export function fixPhpHardcodedSecret(content) {
485
+ const modified = content.replace(
486
+ /((?:secret|api_key|app_key)\s*=\s*)['"]([^'"]{8,})['"]/gi,
487
+ "$1getenv('APP_SECRET') /* TODO: set APP_SECRET in .env */",
488
+ );
489
+ return modified === content ? null : modified;
490
+ }
491
+
492
+ // P32. Validate mime type and extension on upload
493
+ export function fixPhpInsecureUpload(content) {
494
+ const modified = content.replace(
495
+ /move_uploaded_file\(\s*\$_FILES\[['"](\w+)['"]\]\['tmp_name'\],\s*([^)]+)\)/g,
496
+ "/* SECURITY: validate upload before moving */\n$allowed = ['image/jpeg','image/png','application/pdf'];\nif (in_array($_FILES['$1']['type'], \\$allowed) && preg_match('/\\\\.(jpg|png|pdf)$/i', $_FILES['$1']['name'])) {\n move_uploaded_file($_FILES['$1']['tmp_name'], $2);\n}",
497
+ );
498
+ return modified === content ? null : modified;
499
+ }
500
+
501
+ // P33. Add ob_start() for output buffering
502
+ export function fixPhpNoOutputBuffering(content) {
503
+ if (/ob_start/.test(content)) return null;
504
+ const modified = content.replace(
505
+ /(<\?php)/,
506
+ '$1\nob_start(); /* Enable output buffering */',
507
+ );
508
+ return modified === content ? null : modified;
509
+ }
510
+
511
+ // P34. Replace direct SQL string with query builder
512
+ export function fixPhpDirectSql(content) {
513
+ const modified = content.replace(
514
+ /\$\w+->query\(\s*"([^"]*?)\$(\w+)([^"]*?)"\s*\)/g,
515
+ '$$2_stmt = $$1->prepare("$1?$3"); $$2_stmt->execute([$$2])',
516
+ );
517
+ return modified === content ? null : modified;
518
+ }
519
+
520
+ // P35. Regenerate session ID on login
521
+ export function fixPhpSessionRegenerate(content) {
522
+ if (/session_regenerate_id/.test(content)) return null;
523
+ const modified = content.replace(
524
+ /((?:login|authenticate|sign_in)\s*\([^)]*\)\s*\{)/gi,
525
+ '$1\n session_regenerate_id(true); /* SECURITY: prevent session fixation */',
526
+ );
527
+ return modified === content ? null : modified;
528
+ }
529
+
530
+ // ===========================================================================
531
+ // Registry — maps fix IDs to { fn, tier, title }
532
+ // ===========================================================================
533
+ export const rubyPhpFixes = {
534
+ // Ruby (R1-R10) — re-exported from fixer.js
535
+ 'fix-ruby-find-by-sql': { fn: fixRubyFindBySql, tier: 2, title: 'Replace find_by_sql interpolation with parameterized query' },
536
+ 'fix-ruby-html-safe': { fn: fixRubyHtmlSafe, tier: 2, title: 'Replace html_safe with sanitize()' },
537
+ 'fix-ruby-system-call': { fn: fixRubySystemCall, tier: 2, title: 'Replace system() interpolation with argument list' },
538
+ 'fix-ruby-yaml-load': { fn: fixRubyYamlLoad, tier: 1, title: 'Replace YAML.load with YAML.safe_load' },
539
+ 'fix-ruby-marshal-load': { fn: fixRubyMarshalLoad, tier: 2, title: 'Add warning to Marshal.load usage' },
540
+ 'fix-ruby-permit-all': { fn: fixRubyPermitAll, tier: 2, title: 'Replace permit! with explicit field list' },
541
+ 'fix-ruby-skip-csrf': { fn: fixRubySkipCsrf, tier: 2, title: 'Comment out skip_forgery_protection' },
542
+ 'fix-ruby-eval': { fn: fixRubyEvalInput, tier: 2, title: 'Replace Ruby eval() with safer alternative' },
543
+ 'fix-ruby-open-uri': { fn: fixRubyOpenUri, tier: 1, title: 'Replace open(url) with URI.open(url)' },
544
+ 'fix-ruby-weak-hash': { fn: fixRubyWeakHash, tier: 1, title: 'Replace Digest::MD5 with Digest::SHA256' },
545
+ // Ruby (R11-R35)
546
+ 'fix-ruby-hardcoded-secret': { fn: fixRubyHardcodedSecret, tier: 1, title: 'Replace hardcoded secret with ENV lookup' },
547
+ 'fix-ruby-no-strong-params': { fn: fixRubyNoStrongParams, tier: 2, title: 'Add strong params to create calls' },
548
+ 'fix-ruby-raw-output': { fn: fixRubyRawOutput, tier: 2, title: 'Replace raw output with h() helper' },
549
+ 'fix-ruby-exec-input': { fn: fixRubyExecInput, tier: 2, title: 'Add Shellwords.shellescape to backtick commands' },
550
+ 'fix-ruby-backticks-input': { fn: fixRubyBackticksInput, tier: 2, title: 'Replace backticks with Open3.capture3' },
551
+ 'fix-ruby-send-unsafe': { fn: fixRubySendUnsafe, tier: 2, title: 'Replace send() with public_send()' },
552
+ 'fix-ruby-const-get': { fn: fixRubyConstGet, tier: 2, title: 'Add allowlist check to const_get' },
553
+ 'fix-ruby-no-timeout': { fn: fixRubyNoTimeout, tier: 1, title: 'Add timeout to Net::HTTP requests' },
554
+ 'fix-ruby-insecure-ssl': { fn: fixRubyInsecureSsl, tier: 1, title: 'Replace VERIFY_NONE with VERIFY_PEER' },
555
+ 'fix-ruby-no-logging': { fn: fixRubyNoLogging, tier: 1, title: 'Replace puts with Rails.logger in controllers' },
556
+ 'fix-ruby-hardcoded-db': { fn: fixRubyHardcodedDb, tier: 1, title: 'Replace hardcoded DB password with ENV variable' },
557
+ 'fix-ruby-session-secret': { fn: fixRubySessionSecret, tier: 1, title: 'Replace hardcoded session secret with Rails credentials' },
558
+ 'fix-ruby-no-rate-limit': { fn: fixRubyNoRateLimit, tier: 2, title: 'Add Rack::Attack rate limiting suggestion' },
559
+ 'fix-ruby-file-read-input': { fn: fixRubyFileReadInput, tier: 2, title: 'Sanitize path in File.read calls' },
560
+ 'fix-ruby-glob-input': { fn: fixRubyGlobInput, tier: 2, title: 'Escape user input in Dir.glob' },
561
+ 'fix-ruby-regexp-input': { fn: fixRubyRegexpInput, tier: 2, title: 'Wrap user input in Regexp.escape' },
562
+ 'fix-ruby-no-auth': { fn: fixRubyNoAuth, tier: 2, title: 'Add before_action :authenticate_user!' },
563
+ 'fix-ruby-render-inline': { fn: fixRubyRenderInline, tier: 2, title: 'Replace render inline: with template rendering' },
564
+ 'fix-ruby-redirect-input': { fn: fixRubyRedirectInput, tier: 2, title: 'Add URL whitelist to redirect_to' },
565
+ 'fix-ruby-no-encrypted-creds': { fn: fixRubyNoEncryptedCreds, tier: 1, title: 'Replace ENV secrets with Rails.credentials' },
566
+ 'fix-ruby-deprecated-find': { fn: fixRubyDeprecatedFind, tier: 1, title: 'Replace deprecated find(:all) with where()' },
567
+ 'fix-ruby-mass-assignment': { fn: fixRubyMassAssignment, tier: 2, title: 'Replace update_attributes(params) with strong params' },
568
+ 'fix-ruby-no-validation': { fn: fixRubyNoValidation, tier: 2, title: 'Add model validation stubs' },
569
+ 'fix-ruby-hardcoded-email': { fn: fixRubyHardcodedEmail, tier: 1, title: 'Replace hardcoded email with config setting' },
570
+ 'fix-ruby-no-error-handler': { fn: fixRubyNoErrorHandler, tier: 2, title: 'Add rescue_from error handler' },
571
+ // PHP (P1-P10) — re-exported from fixer.js
572
+ 'fix-php-mysql-query': { fn: fixPhpMysqlQuery, tier: 2, title: 'Replace mysql_query with PDO prepared statement' },
573
+ 'fix-php-echo': { fn: fixPhpEchoUnsafe, tier: 1, title: 'Add htmlspecialchars to echoed user input' },
574
+ 'fix-php-eval': { fn: fixPhpEval, tier: 2, title: 'Add security warning to PHP eval()' },
575
+ 'fix-php-exec': { fn: fixPhpExecInput, tier: 2, title: 'Add escapeshellarg to exec() calls' },
576
+ 'fix-php-md5-password': { fn: fixPhpMd5Password, tier: 1, title: 'Replace md5() with password_hash()' },
577
+ 'fix-php-extract': { fn: fixPhpExtractPost, tier: 2, title: 'Replace extract() with explicit assignments' },
578
+ 'fix-php-loose-comparison': { fn: fixPhpLooseComparison, tier: 1, title: 'Replace == with === in PHP auth contexts' },
579
+ 'fix-php-session-config': { fn: fixPhpSessionConfig, tier: 1, title: 'Add secure session cookie settings' },
580
+ 'fix-php-unserialize': { fn: fixPhpUnserialize, tier: 2, title: 'Add allowed_classes restriction to unserialize()' },
581
+ 'fix-php-include': { fn: fixPhpIncludeInput, tier: 2, title: 'Add warning to include with user input' },
582
+ // PHP (P11-P35)
583
+ 'fix-php-system-call': { fn: fixPhpSystemCall, tier: 2, title: 'Add escapeshellcmd to system() calls' },
584
+ 'fix-php-passthru': { fn: fixPhpPassthru, tier: 2, title: 'Replace passthru with proc_open' },
585
+ 'fix-php-preg-e': { fn: fixPhpPreg_e, tier: 2, title: 'Replace preg_replace /e with preg_replace_callback' },
586
+ 'fix-php-global-var': { fn: fixPhpGlobalVar, tier: 2, title: 'Replace global variable with function parameter' },
587
+ 'fix-php-magic-quotes': { fn: fixPhpMagicQuotes, tier: 1, title: 'Remove magic_quotes reliance' },
588
+ 'fix-php-file-get-contents': { fn: fixPhpFileGetContents, tier: 2, title: 'Replace file_get_contents with curl + validation' },
589
+ 'fix-php-hardcoded-db': { fn: fixPhpHardcodedDb, tier: 1, title: 'Replace hardcoded DB password with getenv()' },
590
+ 'fix-php-no-rate-limit': { fn: fixPhpNoRateLimit, tier: 2, title: 'Add throttle middleware to routes' },
591
+ 'fix-php-no-input-filter': { fn: fixPhpNoInputFilter, tier: 2, title: 'Replace $_GET with filter_input()' },
592
+ 'fix-php-weak-random': { fn: fixPhpWeakRandom, tier: 1, title: 'Replace rand() with random_int()' },
593
+ 'fix-php-no-type-hints': { fn: fixPhpNoTypeHints, tier: 1, title: 'Add type hints to function parameters' },
594
+ 'fix-php-no-csrf': { fn: fixPhpNoCsrf, tier: 2, title: 'Add CSRF token to POST forms' },
595
+ 'fix-php-blade-unescaped': { fn: fixPhpBladUnescaped, tier: 1, title: 'Replace {!! !!} with {{ }} in Blade templates' },
596
+ 'fix-php-laravel-raw-query': { fn: fixPhpLaravelRawQuery, tier: 2, title: 'Replace DB::raw with parameterized query' },
597
+ 'fix-php-no-validation': { fn: fixPhpNoValidation, tier: 2, title: 'Add request validation to controller' },
598
+ 'fix-php-env-exposure': { fn: fixPhpEnvExposure, tier: 1, title: 'Remove phpinfo() calls' },
599
+ 'fix-php-debug-mode': { fn: fixPhpDebugMode, tier: 1, title: 'Set APP_DEBUG to false' },
600
+ 'fix-php-open-basedir': { fn: fixPhpOpenBasedir, tier: 1, title: 'Set open_basedir restriction' },
601
+ 'fix-php-allow-url-include': { fn: fixPhpAllowUrlInclude, tier: 1, title: 'Disable allow_url_include' },
602
+ 'fix-php-no-error-display': { fn: fixPhpNoErrorDisplay, tier: 1, title: 'Disable display_errors' },
603
+ 'fix-php-hardcoded-secret': { fn: fixPhpHardcodedSecret, tier: 1, title: 'Replace hardcoded secret with getenv()' },
604
+ 'fix-php-insecure-upload': { fn: fixPhpInsecureUpload, tier: 2, title: 'Validate mime type and extension on upload' },
605
+ 'fix-php-no-output-buffering': { fn: fixPhpNoOutputBuffering, tier: 1, title: 'Add output buffering with ob_start()' },
606
+ 'fix-php-direct-sql': { fn: fixPhpDirectSql, tier: 2, title: 'Replace direct SQL with prepared statement' },
607
+ 'fix-php-session-regenerate': { fn: fixPhpSessionRegenerate, tier: 2, title: 'Regenerate session ID on login' },
608
+ };