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,142 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /**
5
+ * Auto-detect the project's technology stack by examining files.
6
+ */
7
+ export async function detectStack(targetPath) {
8
+ const stack = {
9
+ language: null,
10
+ framework: null,
11
+ runtime: null,
12
+ orm: null,
13
+ database: null,
14
+ hasDocker: false,
15
+ hasCI: false,
16
+ hasTypescript: false,
17
+ hasTests: false,
18
+ packageManager: null,
19
+ dependencies: {},
20
+ devDependencies: {},
21
+ };
22
+
23
+ // Check package.json (Node.js ecosystem)
24
+ const pkgPath = join(targetPath, 'package.json');
25
+ if (existsSync(pkgPath)) {
26
+ try {
27
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
28
+ stack.language = 'javascript';
29
+ stack.runtime = 'node';
30
+ stack.dependencies = pkg.dependencies || {};
31
+ stack.devDependencies = pkg.devDependencies || {};
32
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
33
+
34
+ // Detect framework
35
+ if (allDeps['next']) stack.framework = 'nextjs';
36
+ else if (allDeps['nuxt']) stack.framework = 'nuxt';
37
+ else if (allDeps['express']) stack.framework = 'express';
38
+ else if (allDeps['fastify']) stack.framework = 'fastify';
39
+ else if (allDeps['hono']) stack.framework = 'hono';
40
+ else if (allDeps['koa']) stack.framework = 'koa';
41
+ else if (allDeps['@remix-run/node']) stack.framework = 'remix';
42
+ else if (allDeps['svelte'] || allDeps['@sveltejs/kit']) stack.framework = 'sveltekit';
43
+ else if (allDeps['astro']) stack.framework = 'astro';
44
+ else if (allDeps['react']) stack.framework = 'react';
45
+ else if (allDeps['vue']) stack.framework = 'vue';
46
+
47
+ // Detect ORM
48
+ if (allDeps['prisma'] || allDeps['@prisma/client']) stack.orm = 'prisma';
49
+ else if (allDeps['drizzle-orm']) stack.orm = 'drizzle';
50
+ else if (allDeps['typeorm']) stack.orm = 'typeorm';
51
+ else if (allDeps['sequelize']) stack.orm = 'sequelize';
52
+ else if (allDeps['mongoose']) { stack.orm = 'mongoose'; stack.database = 'mongodb'; }
53
+ else if (allDeps['knex']) stack.orm = 'knex';
54
+
55
+ // Detect database
56
+ if (allDeps['pg'] || allDeps['@supabase/supabase-js']) stack.database = 'postgresql';
57
+ else if (allDeps['mysql2'] || allDeps['mysql']) stack.database = 'mysql';
58
+ else if (allDeps['better-sqlite3'] || allDeps['sqlite3']) stack.database = 'sqlite';
59
+ else if (allDeps['redis'] || allDeps['ioredis']) stack.database = stack.database || 'redis';
60
+
61
+ // Detect package manager
62
+ if (existsSync(join(targetPath, 'pnpm-lock.yaml'))) stack.packageManager = 'pnpm';
63
+ else if (existsSync(join(targetPath, 'yarn.lock'))) stack.packageManager = 'yarn';
64
+ else if (existsSync(join(targetPath, 'bun.lockb'))) stack.packageManager = 'bun';
65
+ else stack.packageManager = 'npm';
66
+
67
+ } catch (e) { /* ignore parse errors */ }
68
+ }
69
+
70
+ // Check Python
71
+ if (existsSync(join(targetPath, 'requirements.txt')) || existsSync(join(targetPath, 'pyproject.toml'))) {
72
+ stack.language = stack.language || 'python';
73
+ stack.runtime = 'python';
74
+ // Try to detect Django/Flask/FastAPI from requirements
75
+ try {
76
+ const reqFile = existsSync(join(targetPath, 'requirements.txt'))
77
+ ? readFileSync(join(targetPath, 'requirements.txt'), 'utf-8')
78
+ : '';
79
+ if (reqFile.includes('django')) stack.framework = 'django';
80
+ else if (reqFile.includes('fastapi')) stack.framework = 'fastapi';
81
+ else if (reqFile.includes('flask')) stack.framework = 'flask';
82
+ } catch (e) { /* ignore */ }
83
+ }
84
+
85
+ // Check Ruby
86
+ if (existsSync(join(targetPath, 'Gemfile'))) {
87
+ stack.language = stack.language || 'ruby';
88
+ stack.runtime = 'ruby';
89
+ try {
90
+ const gemfile = readFileSync(join(targetPath, 'Gemfile'), 'utf-8');
91
+ if (gemfile.includes('rails')) stack.framework = 'rails';
92
+ else if (gemfile.includes('sinatra')) stack.framework = 'sinatra';
93
+ } catch (e) { /* ignore */ }
94
+ }
95
+
96
+ // Check Go
97
+ if (existsSync(join(targetPath, 'go.mod'))) {
98
+ stack.language = stack.language || 'go';
99
+ stack.runtime = 'go';
100
+ }
101
+
102
+ // TypeScript
103
+ if (existsSync(join(targetPath, 'tsconfig.json'))) {
104
+ stack.hasTypescript = true;
105
+ }
106
+
107
+ // Docker
108
+ if (existsSync(join(targetPath, 'Dockerfile')) || existsSync(join(targetPath, 'docker-compose.yml')) || existsSync(join(targetPath, 'docker-compose.yaml'))) {
109
+ stack.hasDocker = true;
110
+ }
111
+
112
+ // CI/CD
113
+ if (existsSync(join(targetPath, '.github/workflows')) || existsSync(join(targetPath, '.gitlab-ci.yml')) || existsSync(join(targetPath, '.circleci'))) {
114
+ stack.hasCI = true;
115
+ }
116
+
117
+ // Tests
118
+ if (existsSync(join(targetPath, 'test')) || existsSync(join(targetPath, 'tests')) || existsSync(join(targetPath, '__tests__')) || existsSync(join(targetPath, 'spec'))) {
119
+ stack.hasTests = true;
120
+ }
121
+
122
+ // Detect library vs application
123
+ // A library typically: has `main` or `exports` in package.json, no `start` script,
124
+ // and the package name doesn't suggest it's an app
125
+ stack.isLibrary = false;
126
+ if (existsSync(pkgPath)) {
127
+ try {
128
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
129
+ const hasEntry = !!(pkg.main || pkg.exports || pkg.module || pkg.files || existsSync(join(targetPath, 'index.js')));
130
+ const hasStart = !!(pkg.scripts?.start || pkg.scripts?.serve || pkg.scripts?.dev);
131
+ const isPrivate = !!pkg.private;
132
+ const isPublished = !!(pkg.name && pkg.version && !isPrivate);
133
+ // Libraries: have an entry point, are published to npm, and don't have a start script
134
+ // Apps: are private and/or have a start script
135
+ if (hasEntry && isPublished && !hasStart) {
136
+ stack.isLibrary = true;
137
+ }
138
+ } catch (e) { /* ignore */ }
139
+ }
140
+
141
+ return stack;
142
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Data-driven fix engine. Each fix is a compact entry:
3
+ * { id, match: RegExp, replace: string|fn, lang: string, tier: 1|2, title: string, safe?: boolean }
4
+ *
5
+ * safe: true = deterministic, low-risk (direct replacements like md5→sha256)
6
+ * safe: false = may need review (adds comments, restructures code)
7
+ * tier 1 fixes are generally safe; tier 2 may need review.
8
+ */
9
+
10
+ export function applyRegistryFix(content, entry) {
11
+ if (!entry.match || !entry.replace) return null;
12
+ const modified = content.replace(entry.match, entry.replace);
13
+ return modified !== content ? modified : null;
14
+ }
15
+
16
+ export function applyAllRegistryFixes(content, filePath, entries) {
17
+ let current = content;
18
+ const applied = [];
19
+ for (const entry of entries) {
20
+ // Filter by language/file extension
21
+ if (entry.lang && !matchesLang(filePath, entry.lang)) continue;
22
+ const result = applyRegistryFix(current, entry);
23
+ if (result) {
24
+ current = result;
25
+ applied.push(entry);
26
+ }
27
+ }
28
+ return { content: current, applied };
29
+ }
30
+
31
+ function matchesLang(filePath, lang) {
32
+ const extMap = {
33
+ js: /\.(js|jsx|ts|tsx|mjs|cjs)$/i,
34
+ py: /\.(py|pyw)$/i,
35
+ go: /\.go$/i,
36
+ rust: /\.rs$/i,
37
+ java: /\.java$/i,
38
+ csharp: /\.(cs|csx)$/i,
39
+ ruby: /\.(rb|rake|gemspec)$/i,
40
+ php: /\.(php|phtml)$/i,
41
+ shell: /\.(sh|bash|zsh|ksh)$/i,
42
+ swift: /\.swift$/i,
43
+ dart: /\.dart$/i,
44
+ any: /./,
45
+ };
46
+ const re = extMap[lang] || extMap.any;
47
+ return re.test(filePath);
48
+ }
@@ -0,0 +1,95 @@
1
+ // Extra fixes: Swift, Dart, Shell, Infrastructure, Docker, CI/CD (80 entries)
2
+ const fixes = [
3
+ // --- Swift (1-15) ---
4
+ { id: 'rfx-swift-001', match: /NSLog\(.*password/gi, replace: '/* SECURITY: do not log passwords */', lang: 'swift', tier: 1, title: 'Remove password from NSLog' },
5
+ { id: 'rfx-swift-002', match: /NSLog\(.*token/gi, replace: '/* SECURITY: do not log tokens */', lang: 'swift', tier: 1, title: 'Remove token from NSLog' },
6
+ { id: 'rfx-swift-003', match: /print\(.*password/gi, replace: '/* SECURITY: do not print passwords */', lang: 'swift', tier: 1, title: 'Remove password from print' },
7
+ { id: 'rfx-swift-004', match: /UserDefaults\.standard\.set\(.*(?:password|token|secret)/gi, replace: (m) => `/* SECURITY: use Keychain, not UserDefaults for secrets */ ${m}`, lang: 'swift', tier: 2, title: 'Use Keychain instead of UserDefaults for secrets' },
8
+ { id: 'rfx-swift-005', match: /allowsArbitraryLoads.*true/g, replace: '/* SECURITY: disable ATS bypass */ allowsArbitraryLoads: false', lang: 'swift', tier: 1, title: 'Disable App Transport Security bypass' },
9
+ { id: 'rfx-swift-006', match: /CC_MD5\(/g, replace: 'CC_SHA256(', lang: 'swift', tier: 1, title: 'Replace MD5 with SHA-256 in Swift' },
10
+ { id: 'rfx-swift-007', match: /\.serverTrust\s*!\s*$/gm, replace: '/* SECURITY: validate server trust properly */ .serverTrust', lang: 'swift', tier: 2, title: 'Flag force-unwrapped server trust' },
11
+ { id: 'rfx-swift-008', match: /URLSession\.shared/g, replace: '/* consider custom URLSession with timeout config */ URLSession.shared', lang: 'swift', tier: 2, title: 'Use custom URLSession with timeout' },
12
+ { id: 'rfx-swift-009', match: /kSecAttrAccessibleAlways\b/g, replace: 'kSecAttrAccessibleWhenUnlockedThisDeviceOnly', lang: 'swift', tier: 1, title: 'Restrict Keychain accessibility' },
13
+ { id: 'rfx-swift-010', match: /canEvaluatePolicy.*\.deviceOwnerAuthentication(?!WithBiometrics)/g, replace: (m) => m.replace('deviceOwnerAuthentication', 'deviceOwnerAuthenticationWithBiometrics'), lang: 'swift', tier: 1, title: 'Require biometrics, not just passcode' },
14
+ { id: 'rfx-swift-011', match: /try!\s/g, replace: '/* avoid force try */ try ', lang: 'swift', tier: 1, title: 'Replace force try with proper error handling' },
15
+ { id: 'rfx-swift-012', match: /as!\s/g, replace: '/* avoid force cast */ as? ', lang: 'swift', tier: 1, title: 'Replace force cast with optional cast' },
16
+ { id: 'rfx-swift-013', match: /\.validate\(\s*false\s*\)/g, replace: '.validate(true)', lang: 'swift', tier: 1, title: 'Enable SSL validation in Swift' },
17
+ { id: 'rfx-swift-014', match: /UIPasteboard\.general\.string\s*=.*(?:password|token|secret)/gi, replace: '/* SECURITY: do not copy secrets to pasteboard */', lang: 'swift', tier: 1, title: 'Prevent secrets on pasteboard' },
18
+ { id: 'rfx-swift-015', match: /isSecureTextEntry\s*=\s*false/g, replace: 'isSecureTextEntry = true', lang: 'swift', tier: 1, title: 'Enable secure text entry for password fields' },
19
+
20
+ // --- Dart/Flutter (16-30) ---
21
+ { id: 'rfx-dart-001', match: /print\(.*password/gi, replace: '/* SECURITY: do not print passwords */', lang: 'dart', tier: 1, title: 'Remove password from print' },
22
+ { id: 'rfx-dart-002', match: /print\(.*token/gi, replace: '/* SECURITY: do not print tokens */', lang: 'dart', tier: 1, title: 'Remove token from print' },
23
+ { id: 'rfx-dart-003', match: /SharedPreferences.*(?:password|token|secret|apiKey)/gi, replace: (m) => `/* SECURITY: use flutter_secure_storage, not SharedPreferences */ ${m}`, lang: 'dart', tier: 2, title: 'Use secure storage for secrets in Dart' },
24
+ { id: 'rfx-dart-004', match: /HttpClient\(\)\.badCertificateCallback\s*=\s*\([^)]*\)\s*=>\s*true/g, replace: '/* SECURITY: do not bypass cert validation */ HttpClient().badCertificateCallback = (cert, host, port) => false', lang: 'dart', tier: 1, title: 'Enable certificate validation in Dart' },
25
+ { id: 'rfx-dart-005', match: /md5\.convert\(/g, replace: 'sha256.convert(', lang: 'dart', tier: 1, title: 'Replace MD5 with SHA-256 in Dart' },
26
+ { id: 'rfx-dart-006', match: /sha1\.convert\(/g, replace: 'sha256.convert(', lang: 'dart', tier: 1, title: 'Replace SHA-1 with SHA-256 in Dart' },
27
+ { id: 'rfx-dart-007', match: /Random\(\)/g, replace: 'Random.secure()', lang: 'dart', tier: 1, title: 'Use Random.secure() in Dart' },
28
+ { id: 'rfx-dart-008', match: /debugPrint\(.*(?:password|token|secret)/gi, replace: '/* SECURITY: do not debug-print secrets */', lang: 'dart', tier: 1, title: 'Remove secrets from debugPrint' },
29
+ { id: 'rfx-dart-009', match: /assert\(.*(?:isLoggedIn|authenticated)/gi, replace: (m) => `/* use proper auth check, not assert (stripped in release) */ ${m}`, lang: 'dart', tier: 2, title: 'Replace auth assert with proper check' },
30
+ { id: 'rfx-dart-010', match: /kDebugMode.*true/g, replace: 'kDebugMode /* already controlled by build mode */', lang: 'dart', tier: 1, title: 'Confirm debug mode usage' },
31
+ { id: 'rfx-dart-011', match: /\.allowAutoSignedCert\s*=\s*true/g, replace: '.allowAutoSignedCert = false', lang: 'dart', tier: 1, title: 'Disable auto-signed certs in Dart' },
32
+ { id: 'rfx-dart-012', match: /WebView\(.*javascriptMode:\s*JavascriptMode\.unrestricted/g, replace: (m) => `/* SECURITY: restrict JS in WebView */ ${m.replace('unrestricted', 'disabled')}`, lang: 'dart', tier: 1, title: 'Restrict JavaScript in Dart WebView' },
33
+ { id: 'rfx-dart-013', match: /html:\s*['"]<script/gi, replace: (m) => `/* SECURITY: XSS in WebView */ ${m}`, lang: 'dart', tier: 2, title: 'Flag XSS in Dart WebView HTML' },
34
+ { id: 'rfx-dart-014', match: /Uri\.parse\(\s*(?:input|userInput|data)/g, replace: '/* SECURITY: validate URL */ Uri.parse(input', lang: 'dart', tier: 2, title: 'Validate user URL input in Dart' },
35
+ { id: 'rfx-dart-015', match: /Process\.run\(\s*['"]sh['"]\s*,\s*\['-c'/g, replace: '/* SECURITY: command injection risk */ Process.run("sh", ["-c"', lang: 'dart', tier: 2, title: 'Flag shell execution in Dart' },
36
+
37
+ // --- Shell (31-45) ---
38
+ { id: 'rfx-sh-001', match: /curl\s+[^|]*\|\s*(?:ba)?sh/g, replace: '/* SECURITY: pipe-to-shell is dangerous — download, verify, then run */', lang: 'shell', tier: 2, title: 'Flag curl pipe to shell' },
39
+ { id: 'rfx-sh-002', match: /wget\s+[^|]*\|\s*(?:ba)?sh/g, replace: '/* SECURITY: pipe-to-shell is dangerous */', lang: 'shell', tier: 2, title: 'Flag wget pipe to shell' },
40
+ { id: 'rfx-sh-003', match: /chmod\s+777\b/g, replace: 'chmod 755', lang: 'shell', tier: 1, title: 'Reduce permissions from 777 to 755' },
41
+ { id: 'rfx-sh-004', match: /chmod\s+666\b/g, replace: 'chmod 644', lang: 'shell', tier: 1, title: 'Reduce permissions from 666 to 644' },
42
+ { id: 'rfx-sh-005', match: /eval\s+"\$\{?\w+\}?"/g, replace: '/* SECURITY: eval with variables is injection risk */', lang: 'shell', tier: 2, title: 'Flag eval with variables in shell' },
43
+ { id: 'rfx-sh-006', match: /^\s*#!/gm, replace: '#!/', lang: 'shell', tier: 1, title: 'Normalize shebang' },
44
+ { id: 'rfx-sh-007', match: /rm\s+-rf\s+\/(?!\w)/g, replace: '/* DANGER: rm -rf / — BLOCKED */', lang: 'shell', tier: 1, title: 'Block rm -rf /' },
45
+ { id: 'rfx-sh-008', match: /rm\s+-rf\s+"\$\{?\w*\}?\/"/g, replace: (m) => `/* SECURITY: validate variable before rm -rf */ ${m}`, lang: 'shell', tier: 2, title: 'Flag rm -rf with variable path' },
46
+ { id: 'rfx-sh-009', match: /mktemp\s+\/tmp\/\w+/g, replace: 'mktemp /tmp/XXXXXXXX', lang: 'shell', tier: 1, title: 'Use random suffix in mktemp' },
47
+ { id: 'rfx-sh-010', match: /echo\s+.*password/gi, replace: '# SECURITY: do not echo passwords', lang: 'shell', tier: 1, title: 'Remove password echo in shell' },
48
+ { id: 'rfx-sh-011', match: /echo\s+.*(?:SECRET|TOKEN|API_KEY)\s*=/gi, replace: '# SECURITY: do not echo secrets', lang: 'shell', tier: 1, title: 'Remove secret echo in shell' },
49
+ { id: 'rfx-sh-012', match: /scp\s+-o\s+StrictHostKeyChecking=no/g, replace: 'scp -o StrictHostKeyChecking=yes', lang: 'shell', tier: 1, title: 'Enable SSH host key checking' },
50
+ { id: 'rfx-sh-013', match: /ssh\s+-o\s+StrictHostKeyChecking=no/g, replace: 'ssh -o StrictHostKeyChecking=yes', lang: 'shell', tier: 1, title: 'Enable SSH host key checking' },
51
+ { id: 'rfx-sh-014', match: /tar\s+.*--absolute-names/g, replace: (m) => `/* SECURITY: absolute paths in tar can overwrite system files */ ${m}`, lang: 'shell', tier: 2, title: 'Flag tar with absolute names' },
52
+ { id: 'rfx-sh-015', match: /nc\s+-l.*-e\s/g, replace: '/* SECURITY: netcat reverse shell pattern */', lang: 'shell', tier: 2, title: 'Flag netcat reverse shell' },
53
+
54
+ // --- Docker (46-60) ---
55
+ { id: 'rfx-docker-001', match: /FROM\s+\S+:latest/g, replace: (m) => m.replace(':latest', ':specific-version /* pin image version */'), lang: 'any', tier: 1, title: 'Pin Docker base image version' },
56
+ { id: 'rfx-docker-002', match: /USER\s+root/g, replace: 'USER nonroot', lang: 'any', tier: 1, title: 'Run Docker container as non-root' },
57
+ { id: 'rfx-docker-003', match: /--privileged/g, replace: '/* SECURITY: avoid --privileged */ --cap-add=SPECIFIC_CAP', lang: 'any', tier: 2, title: 'Remove Docker privileged flag' },
58
+ { id: 'rfx-docker-004', match: /EXPOSE\s+22\b/g, replace: '# SECURITY: do not expose SSH in container\n# EXPOSE 22', lang: 'any', tier: 1, title: 'Remove SSH port exposure in Docker' },
59
+ { id: 'rfx-docker-005', match: /ENV\s+(?:PASSWORD|SECRET|API_KEY|TOKEN)\s*=\s*\S+/gi, replace: '# SECURITY: use --env-file or secrets, not ENV for secrets', lang: 'any', tier: 1, title: 'Remove hardcoded secrets from Dockerfile ENV' },
60
+ { id: 'rfx-docker-006', match: /ADD\s+https?:/g, replace: '# SECURITY: use COPY + curl with checksum instead of ADD\nADD https:', lang: 'any', tier: 2, title: 'Replace Docker ADD URL with COPY' },
61
+ { id: 'rfx-docker-007', match: /apt-get\s+install(?![^&]*--no-install-recommends)/g, replace: 'apt-get install --no-install-recommends', lang: 'any', tier: 1, title: 'Add --no-install-recommends to apt-get' },
62
+ { id: 'rfx-docker-008', match: /COPY\s+\.\s+\./g, replace: '# Use .dockerignore to exclude secrets\nCOPY . .', lang: 'any', tier: 2, title: 'Flag COPY . . without .dockerignore note' },
63
+ { id: 'rfx-docker-009', match: /--security-opt\s+seccomp=unconfined/g, replace: '/* SECURITY: do not disable seccomp */', lang: 'any', tier: 2, title: 'Remove seccomp disable' },
64
+ { id: 'rfx-docker-010', match: /--net=host/g, replace: '/* SECURITY: avoid host networking */ --net=bridge', lang: 'any', tier: 1, title: 'Replace Docker host networking with bridge' },
65
+ { id: 'rfx-docker-011', match: /HEALTHCHECK\s+NONE/g, replace: 'HEALTHCHECK CMD curl -f http://localhost/ || exit 1', lang: 'any', tier: 1, title: 'Add Docker health check' },
66
+ { id: 'rfx-docker-012', match: /volumes:\s*\n\s*-\s*\/:/gm, replace: '# SECURITY: do not mount root filesystem\nvolumes:\n - /app:', lang: 'any', tier: 2, title: 'Restrict Docker volume mount' },
67
+ { id: 'rfx-docker-013', match: /cap_add:\s*\n\s*-\s*ALL/gm, replace: '# SECURITY: add only needed capabilities\ncap_add:\n - NET_BIND_SERVICE', lang: 'any', tier: 1, title: 'Restrict Docker capabilities' },
68
+ { id: 'rfx-docker-014', match: /read_only:\s*false/g, replace: 'read_only: true', lang: 'any', tier: 1, title: 'Enable Docker read-only filesystem' },
69
+ { id: 'rfx-docker-015', match: /pids_limit:\s*-1/g, replace: 'pids_limit: 100', lang: 'any', tier: 1, title: 'Set Docker PID limit' },
70
+
71
+ // --- CI/CD (61-75) ---
72
+ { id: 'rfx-ci-001', match: /permissions:\s*write-all/g, replace: 'permissions: read-all', lang: 'any', tier: 1, title: 'Restrict CI permissions from write-all' },
73
+ { id: 'rfx-ci-002', match: /actions\/checkout@v[12]\b/g, replace: 'actions/checkout@v4', lang: 'any', tier: 1, title: 'Upgrade actions/checkout to v4' },
74
+ { id: 'rfx-ci-003', match: /echo\s+.*\$\{\{\s*secrets\./g, replace: '# SECURITY: do not echo secrets in CI logs', lang: 'any', tier: 1, title: 'Remove secret echoing in CI' },
75
+ { id: 'rfx-ci-004', match: /\$\{\{\s*github\.event\.issue\.body\s*\}\}/g, replace: '/* SECURITY: user-controlled input — sanitize */', lang: 'any', tier: 2, title: 'Flag unsanitized GitHub event body' },
76
+ { id: 'rfx-ci-005', match: /\$\{\{\s*github\.event\.pull_request\.title\s*\}\}/g, replace: '/* SECURITY: user-controlled input — sanitize */', lang: 'any', tier: 2, title: 'Flag unsanitized PR title in CI' },
77
+ { id: 'rfx-ci-006', match: /npm\s+publish(?!\s+--dry-run)/g, replace: 'npm publish --provenance', lang: 'any', tier: 1, title: 'Add provenance to npm publish' },
78
+ { id: 'rfx-ci-007', match: /pip\s+install(?!\s+--require-hashes)/g, replace: (m) => `${m} /* consider --require-hashes */`, lang: 'any', tier: 2, title: 'Suggest pip --require-hashes' },
79
+ { id: 'rfx-ci-008', match: /continue-on-error:\s*true/g, replace: '# review: continue-on-error may hide failures\ncontinue-on-error: true', lang: 'any', tier: 2, title: 'Flag continue-on-error in CI' },
80
+ { id: 'rfx-ci-009', match: /if:\s*always\(\)/g, replace: 'if: always() # ensure this is intentional', lang: 'any', tier: 2, title: 'Flag always() condition in CI' },
81
+ { id: 'rfx-ci-010', match: /runs-on:\s*self-hosted(?!\s*#)/g, replace: 'runs-on: self-hosted # ensure runner is hardened', lang: 'any', tier: 2, title: 'Note self-hosted runner security' },
82
+ { id: 'rfx-ci-011', match: /ACTIONS_ALLOW_UNSECURE_COMMANDS.*true/g, replace: '# SECURITY: do not allow unsecure commands\n# ACTIONS_ALLOW_UNSECURE_COMMANDS: true', lang: 'any', tier: 1, title: 'Disable unsecure CI commands' },
83
+ { id: 'rfx-ci-012', match: /npm_token.*\$\{\{\s*secrets/gi, replace: (m) => m, lang: 'any', tier: 1, title: 'Confirm NPM token from secrets (ok)' },
84
+ { id: 'rfx-ci-013', match: /--no-verify/g, replace: '/* avoid --no-verify — hooks exist for a reason */', lang: 'any', tier: 2, title: 'Flag --no-verify in CI' },
85
+ { id: 'rfx-ci-014', match: /git push --force/g, replace: '/* SECURITY: avoid force push */ git push', lang: 'any', tier: 2, title: 'Flag force push in CI' },
86
+ { id: 'rfx-ci-015', match: /curl\s+.*\|\s*(?:ba)?sh/g, replace: '# SECURITY: download, verify checksum, then execute', lang: 'any', tier: 2, title: 'Flag curl-pipe-to-shell in CI' },
87
+
88
+ // --- Terraform/IaC (76-80) ---
89
+ { id: 'rfx-iac-001', match: /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\]/g, replace: (m) => m.replace('0.0.0.0/0', '10.0.0.0/8 /* restrict CIDR */'), lang: 'any', tier: 1, title: 'Restrict Terraform ingress CIDR' },
90
+ { id: 'rfx-iac-002', match: /acl\s*=\s*"public-read"/g, replace: 'acl = "private"', lang: 'any', tier: 1, title: 'Set S3 bucket to private' },
91
+ { id: 'rfx-iac-003', match: /encrypted\s*=\s*false/g, replace: 'encrypted = true', lang: 'any', tier: 1, title: 'Enable encryption in Terraform' },
92
+ { id: 'rfx-iac-004', match: /publicly_accessible\s*=\s*true/g, replace: 'publicly_accessible = false', lang: 'any', tier: 1, title: 'Disable public access in Terraform' },
93
+ { id: 'rfx-iac-005', match: /versioning\s*\{[^}]*enabled\s*=\s*false/g, replace: (m) => m.replace('enabled = false', 'enabled = true'), lang: 'any', tier: 1, title: 'Enable S3 versioning' },
94
+ ];
95
+ export default fixes;
@@ -0,0 +1,77 @@
1
+ // Data-driven Go + Rust auto-fixes (70 entries: 35 Go + 35 Rust)
2
+ const fixes = [
3
+ // --- Go (1-35) ---
4
+ { id: 'rfx-go-001', match: /fmt\.Sprintf\("SELECT\s+[^"]*%s/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'go', tier: 2, title: 'Flag Go SQL string formatting' },
5
+ { id: 'rfx-go-002', match: /fmt\.Sprintf\("INSERT\s+[^"]*%s/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'go', tier: 2, title: 'Flag Go SQL INSERT formatting' },
6
+ { id: 'rfx-go-003', match: /fmt\.Sprintf\("UPDATE\s+[^"]*%s/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'go', tier: 2, title: 'Flag Go SQL UPDATE formatting' },
7
+ { id: 'rfx-go-004', match: /fmt\.Sprintf\("DELETE\s+[^"]*%s/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'go', tier: 2, title: 'Flag Go SQL DELETE formatting' },
8
+ { id: 'rfx-go-005', match: /exec\.Command\(\s*fmt\.Sprintf/g, replace: '/* SECURITY: command injection risk */ exec.Command(fmt.Sprintf', lang: 'go', tier: 2, title: 'Flag Go command injection' },
9
+ { id: 'rfx-go-006', match: /exec\.Command\(\s*"sh"\s*,\s*"-c"\s*,/g, replace: '/* SECURITY: avoid sh -c — use direct exec */ exec.Command("sh", "-c",', lang: 'go', tier: 2, title: 'Flag shell execution in Go' },
10
+ { id: 'rfx-go-007', match: /filepath\.Join\(\s*\w+\s*,\s*r\.(?:URL\.Query|FormValue|PostFormValue)/g, replace: (m) => `/* SECURITY: path traversal — validate input */ ${m}`, lang: 'go', tier: 2, title: 'Flag path traversal in Go' },
11
+ { id: 'rfx-go-008', match: /tls\.Config\{[^}]*MinVersion:\s*tls\.VersionTLS1[01]/g, replace: (m) => m.replace(/MinVersion:\s*tls\.VersionTLS1[01]/, 'MinVersion: tls.VersionTLS12'), lang: 'go', tier: 1, title: 'Upgrade Go TLS minimum to 1.2' },
12
+ { id: 'rfx-go-009', match: /tls\.Config\{[^}]*InsecureSkipVerify:\s*true/g, replace: (m) => m.replace('InsecureSkipVerify: true', 'InsecureSkipVerify: false'), lang: 'go', tier: 1, title: 'Enable TLS verification in Go' },
13
+ { id: 'rfx-go-010', match: /md5\.Sum\(/g, replace: 'sha256.Sum256(', lang: 'go', tier: 1, title: 'Replace Go MD5 with SHA-256' },
14
+ { id: 'rfx-go-011', match: /sha1\.Sum\(/g, replace: 'sha256.Sum256(', lang: 'go', tier: 1, title: 'Replace Go SHA-1 with SHA-256' },
15
+ { id: 'rfx-go-012', match: /md5\.New\(\)/g, replace: 'sha256.New()', lang: 'go', tier: 1, title: 'Replace Go md5.New with sha256.New' },
16
+ { id: 'rfx-go-013', match: /math\/rand/g, replace: 'crypto/rand', lang: 'go', tier: 1, title: 'Replace math/rand with crypto/rand' },
17
+ { id: 'rfx-go-014', match: /rand\.Intn\(/g, replace: '/* SECURITY: use crypto/rand for security */ rand.Intn(', lang: 'go', tier: 2, title: 'Flag math/rand for security use' },
18
+ { id: 'rfx-go-015', match: /http\.ListenAndServe\(/g, replace: 'http.ListenAndServeTLS(', lang: 'go', tier: 2, title: 'Suggest HTTPS in Go server' },
19
+ { id: 'rfx-go-016', match: /\bdefer\s+rows\.Close\(\)/g, replace: 'defer rows.Close()', lang: 'go', tier: 1, title: 'Confirm deferred rows.Close' },
20
+ { id: 'rfx-go-017', match: /http\.Client\{\s*\}/g, replace: 'http.Client{Timeout: 30 * time.Second}', lang: 'go', tier: 1, title: 'Add timeout to Go HTTP client' },
21
+ { id: 'rfx-go-018', match: /http\.DefaultClient/g, replace: '&http.Client{Timeout: 30 * time.Second}', lang: 'go', tier: 1, title: 'Replace DefaultClient with timeout client' },
22
+ { id: 'rfx-go-019', match: /os\.Getenv\("(PASSWORD|SECRET|API_KEY|TOKEN)"\)/g, replace: (m, key) => `os.Getenv("${key}") /* ensure set in environment */`, lang: 'go', tier: 1, title: 'Note env var dependency for secrets' },
23
+ { id: 'rfx-go-020', match: /template\.New\(/g, replace: '/* Use html/template for web output */ template.New(', lang: 'go', tier: 2, title: 'Suggest html/template over text/template' },
24
+ { id: 'rfx-go-021', match: /text\/template/g, replace: 'html/template', lang: 'go', tier: 1, title: 'Replace text/template with html/template' },
25
+ { id: 'rfx-go-022', match: /w\.Header\(\)\.Set\("Access-Control-Allow-Origin",\s*"\*"\)/g, replace: 'w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))', lang: 'go', tier: 1, title: 'Restrict Go CORS wildcard' },
26
+ { id: 'rfx-go-023', match: /os\.MkdirAll\([^,]+,\s*0o?777\)/g, replace: (m) => m.replace(/0o?777/, '0o755'), lang: 'go', tier: 1, title: 'Reduce Go directory permissions' },
27
+ { id: 'rfx-go-024', match: /os\.WriteFile\([^,]+,\s*[^,]+,\s*0o?666\)/g, replace: (m) => m.replace(/0o?666/, '0o644'), lang: 'go', tier: 1, title: 'Reduce Go file permissions' },
28
+ { id: 'rfx-go-025', match: /log\.Printf?\(.*password/gi, replace: (m) => m.replace(/password\w*/gi, '"[REDACTED]"'), lang: 'go', tier: 1, title: 'Redact passwords in Go logs' },
29
+ { id: 'rfx-go-026', match: /log\.Printf?\(.*token/gi, replace: (m) => m.replace(/token\w*/gi, '"[REDACTED]"'), lang: 'go', tier: 1, title: 'Redact tokens in Go logs' },
30
+ { id: 'rfx-go-027', match: /http\.Redirect\(\s*w\s*,\s*r\s*,\s*r\.(URL\.Query|FormValue)/g, replace: (m) => `/* SECURITY: open redirect risk */ ${m}`, lang: 'go', tier: 2, title: 'Flag Go open redirect' },
31
+ { id: 'rfx-go-028', match: /json\.NewDecoder\(r\.Body\)\.Decode/g, replace: '/* limit request body size */ json.NewDecoder(io.LimitReader(r.Body, 1048576)).Decode', lang: 'go', tier: 1, title: 'Limit request body size in Go' },
32
+ { id: 'rfx-go-029', match: /http\.HandleFunc\(/g, replace: 'http.HandleFunc( /* ensure auth middleware */', lang: 'go', tier: 2, title: 'Remind about auth middleware' },
33
+ { id: 'rfx-go-030', match: /ioutil\.ReadFile\(\s*r\.(URL|FormValue)/g, replace: (m) => `/* SECURITY: path traversal risk */ ${m}`, lang: 'go', tier: 2, title: 'Flag file read with user input' },
34
+ { id: 'rfx-go-031', match: /des\./g, replace: '/* SECURITY: DES is broken — use AES */ des.', lang: 'go', tier: 2, title: 'Flag DES usage in Go' },
35
+ { id: 'rfx-go-032', match: /rc4\./g, replace: '/* SECURITY: RC4 is broken — use AES */ rc4.', lang: 'go', tier: 2, title: 'Flag RC4 usage in Go' },
36
+ { id: 'rfx-go-033', match: /gin\.SetMode\(\s*gin\.DebugMode\s*\)/g, replace: 'gin.SetMode(gin.ReleaseMode)', lang: 'go', tier: 1, title: 'Set Gin to release mode' },
37
+ { id: 'rfx-go-034', match: /\.NoRoute\(\s*func/g, replace: '.NoRoute(func', lang: 'go', tier: 1, title: 'Confirm 404 handler exists' },
38
+ { id: 'rfx-go-035', match: /panic\(\s*err\)/g, replace: '/* avoid panic in production */ log.Fatal(err)', lang: 'go', tier: 1, title: 'Replace panic with log.Fatal' },
39
+
40
+ // --- Rust (36-70) ---
41
+ { id: 'rfx-rs-001', match: /format!\("SELECT\s+[^"]*\{/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'rust', tier: 2, title: 'Flag Rust SQL format! injection' },
42
+ { id: 'rfx-rs-002', match: /format!\("INSERT\s+[^"]*\{/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'rust', tier: 2, title: 'Flag Rust SQL INSERT format!' },
43
+ { id: 'rfx-rs-003', match: /format!\("UPDATE\s+[^"]*\{/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'rust', tier: 2, title: 'Flag Rust SQL UPDATE format!' },
44
+ { id: 'rfx-rs-004', match: /format!\("DELETE\s+[^"]*\{/g, replace: (m) => `/* SECURITY: use parameterized query */ ${m}`, lang: 'rust', tier: 2, title: 'Flag Rust SQL DELETE format!' },
45
+ { id: 'rfx-rs-005', match: /\.unwrap\(\)/g, replace: '.expect("TODO: handle error")', lang: 'rust', tier: 1, title: 'Replace unwrap with expect' },
46
+ { id: 'rfx-rs-006', match: /\.unwrap_or\(\s*""\s*\)/g, replace: '.unwrap_or_default()', lang: 'rust', tier: 1, title: 'Use unwrap_or_default' },
47
+ { id: 'rfx-rs-007', match: /unsafe\s*\{/g, replace: '/* SAFETY: document why unsafe is needed */ unsafe {', lang: 'rust', tier: 2, title: 'Require safety comment for unsafe block' },
48
+ { id: 'rfx-rs-008', match: /std::process::Command::new\(\s*&?format!/g, replace: '/* SECURITY: command injection risk */ std::process::Command::new(&format!', lang: 'rust', tier: 2, title: 'Flag Rust command injection' },
49
+ { id: 'rfx-rs-009', match: /Md5::new\(\)/g, replace: 'Sha256::new()', lang: 'rust', tier: 1, title: 'Replace Rust MD5 with SHA-256' },
50
+ { id: 'rfx-rs-010', match: /Sha1::new\(\)/g, replace: 'Sha256::new()', lang: 'rust', tier: 1, title: 'Replace Rust SHA-1 with SHA-256' },
51
+ { id: 'rfx-rs-011', match: /use\s+md5/g, replace: 'use sha2', lang: 'rust', tier: 1, title: 'Replace md5 crate with sha2' },
52
+ { id: 'rfx-rs-012', match: /thread_rng\(\)/g, replace: 'OsRng', lang: 'rust', tier: 1, title: 'Replace thread_rng with OsRng for security' },
53
+ { id: 'rfx-rs-013', match: /rand::random/g, replace: '/* use rand::rngs::OsRng for security */ rand::random', lang: 'rust', tier: 2, title: 'Flag rand::random for security use' },
54
+ { id: 'rfx-rs-014', match: /\.danger_accept_invalid_certs\(\s*true\s*\)/g, replace: '.danger_accept_invalid_certs(false)', lang: 'rust', tier: 1, title: 'Enable Rust TLS cert verification' },
55
+ { id: 'rfx-rs-015', match: /\.danger_accept_invalid_hostnames\(\s*true\s*\)/g, replace: '.danger_accept_invalid_hostnames(false)', lang: 'rust', tier: 1, title: 'Enable Rust hostname verification' },
56
+ { id: 'rfx-rs-016', match: /std::fs::set_permissions.*Permissions::from_mode\(0o777\)/g, replace: (m) => m.replace('0o777', '0o755'), lang: 'rust', tier: 1, title: 'Reduce Rust file permissions' },
57
+ { id: 'rfx-rs-017', match: /panic!\(/g, replace: '/* avoid panic in production */ log::error!(', lang: 'rust', tier: 2, title: 'Replace panic! with log::error!' },
58
+ { id: 'rfx-rs-018', match: /todo!\(\)/g, replace: 'unimplemented!("TODO: implement this")', lang: 'rust', tier: 1, title: 'Replace todo! with explicit unimplemented' },
59
+ { id: 'rfx-rs-019', match: /println!\(".*password/gi, replace: '/* SECURITY: do not print passwords */', lang: 'rust', tier: 1, title: 'Remove password printing in Rust' },
60
+ { id: 'rfx-rs-020', match: /println!\(".*secret/gi, replace: '/* SECURITY: do not print secrets */', lang: 'rust', tier: 1, title: 'Remove secret printing in Rust' },
61
+ { id: 'rfx-rs-021', match: /println!\(".*token/gi, replace: '/* SECURITY: do not print tokens */', lang: 'rust', tier: 1, title: 'Remove token printing in Rust' },
62
+ { id: 'rfx-rs-022', match: /env!?\("(PASSWORD|SECRET|API_KEY|TOKEN)"\)/g, replace: (m) => `std::env::var("$1").expect("$1 must be set")`, lang: 'rust', tier: 1, title: 'Use runtime env var for secrets' },
63
+ { id: 'rfx-rs-023', match: /CorsLayer::permissive\(\)/g, replace: 'CorsLayer::new().allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())', lang: 'rust', tier: 1, title: 'Restrict Rust CORS from permissive' },
64
+ { id: 'rfx-rs-024', match: /\.allow_origin\(\s*Any\s*\)/g, replace: '.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())', lang: 'rust', tier: 1, title: 'Replace CORS Any origin' },
65
+ { id: 'rfx-rs-025', match: /Duration::from_secs\(\s*0\s*\)/g, replace: 'Duration::from_secs(30)', lang: 'rust', tier: 1, title: 'Set non-zero timeout duration' },
66
+ { id: 'rfx-rs-026', match: /serde_json::from_str\(\s*&?body/g, replace: '/* validate input size before parsing */ serde_json::from_str(&body', lang: 'rust', tier: 2, title: 'Validate body size before JSON parsing' },
67
+ { id: 'rfx-rs-027', match: /Path::new\(\s*&?(?:query|param|input)/g, replace: '/* SECURITY: validate path component */ Path::new(&query', lang: 'rust', tier: 2, title: 'Flag path with user input' },
68
+ { id: 'rfx-rs-028', match: /std::fs::read_to_string\(\s*&?(?:query|param|input)/g, replace: '/* SECURITY: path traversal risk */ std::fs::read_to_string(&query', lang: 'rust', tier: 2, title: 'Flag file read with user input' },
69
+ { id: 'rfx-rs-029', match: /Regex::new\(\s*&?(?:query|param|input)/g, replace: '/* SECURITY: ReDoS risk */ Regex::new(&regex::escape(&query', lang: 'rust', tier: 2, title: 'Escape user input in Rust regex' },
70
+ { id: 'rfx-rs-030', match: /bincode::deserialize/g, replace: '/* SECURITY: validate input before deserializing */ bincode::deserialize', lang: 'rust', tier: 2, title: 'Flag bincode deserialization' },
71
+ { id: 'rfx-rs-031', match: /serde_yaml::from_str/g, replace: '/* validate YAML source is trusted */ serde_yaml::from_str', lang: 'rust', tier: 2, title: 'Flag YAML deserialization' },
72
+ { id: 'rfx-rs-032', match: /\.timeout\(\s*None\s*\)/g, replace: '.timeout(Some(Duration::from_secs(30)))', lang: 'rust', tier: 1, title: 'Set timeout instead of None' },
73
+ { id: 'rfx-rs-033', match: /actix_web::HttpServer::new.*\.workers\(\s*1\s*\)/g, replace: (m) => m.replace('.workers(1)', '.workers(num_cpus::get())'), lang: 'rust', tier: 1, title: 'Use CPU count for Actix workers' },
74
+ { id: 'rfx-rs-034', match: /\.cookie_secure\(\s*false\s*\)/g, replace: '.cookie_secure(true)', lang: 'rust', tier: 1, title: 'Enable secure cookies in Rust' },
75
+ { id: 'rfx-rs-035', match: /\.cookie_http_only\(\s*false\s*\)/g, replace: '.cookie_http_only(true)', lang: 'rust', tier: 1, title: 'Enable httpOnly cookies in Rust' },
76
+ ];
77
+ export default fixes;
@@ -0,0 +1,77 @@
1
+ // Data-driven Java + C# auto-fixes (70 entries: 35 Java + 35 C#)
2
+ const fixes = [
3
+ // --- Java (1-35) ---
4
+ { id: 'rfx-java-001', match: /Statement\s+\w+\s*=.*createStatement/g, replace: (m) => `/* SECURITY: use PreparedStatement instead */ ${m}`, lang: 'java', tier: 2, title: 'Flag Java Statement (use PreparedStatement)' },
5
+ { id: 'rfx-java-002', match: /executeQuery\(\s*"SELECT\s+[^"]*"\s*\+/g, replace: (m) => `/* SECURITY: SQL injection — use parameterized query */ ${m}`, lang: 'java', tier: 2, title: 'Flag Java SQL concatenation' },
6
+ { id: 'rfx-java-003', match: /executeUpdate\(\s*"(?:INSERT|UPDATE|DELETE)\s+[^"]*"\s*\+/g, replace: (m) => `/* SECURITY: SQL injection — use parameterized query */ ${m}`, lang: 'java', tier: 2, title: 'Flag Java SQL mutation concatenation' },
7
+ { id: 'rfx-java-004', match: /Runtime\.getRuntime\(\)\.exec\(\s*"[^"]*"\s*\+/g, replace: (m) => `/* SECURITY: command injection */ ${m}`, lang: 'java', tier: 2, title: 'Flag Java command injection' },
8
+ { id: 'rfx-java-005', match: /new\s+ProcessBuilder\(\s*"sh"\s*,\s*"-c"/g, replace: '/* SECURITY: avoid shell execution */ new ProcessBuilder("sh", "-c"', lang: 'java', tier: 2, title: 'Flag Java shell execution' },
9
+ { id: 'rfx-java-006', match: /ObjectInputStream\(/g, replace: '/* SECURITY: deserialization risk — validate input */ ObjectInputStream(', lang: 'java', tier: 2, title: 'Flag Java ObjectInputStream' },
10
+ { id: 'rfx-java-007', match: /XMLInputFactory\.newInstance\(\)/g, replace: '/* disable XXE */ XMLInputFactory.newInstance() /* call setProperty(XMLInputFactory.SUPPORT_DTD, false) */', lang: 'java', tier: 2, title: 'Flag Java XXE risk' },
11
+ { id: 'rfx-java-008', match: /DocumentBuilderFactory\.newInstance\(\)/g, replace: '/* disable XXE: setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) */ DocumentBuilderFactory.newInstance()', lang: 'java', tier: 2, title: 'Flag Java XXE in DocumentBuilder' },
12
+ { id: 'rfx-java-009', match: /SAXParserFactory\.newInstance\(\)/g, replace: '/* disable XXE */ SAXParserFactory.newInstance()', lang: 'java', tier: 2, title: 'Flag Java XXE in SAXParser' },
13
+ { id: 'rfx-java-010', match: /MessageDigest\.getInstance\("MD5"\)/g, replace: 'MessageDigest.getInstance("SHA-256")', lang: 'java', tier: 1, title: 'Replace Java MD5 with SHA-256' },
14
+ { id: 'rfx-java-011', match: /MessageDigest\.getInstance\("SHA-1"\)/g, replace: 'MessageDigest.getInstance("SHA-256")', lang: 'java', tier: 1, title: 'Replace Java SHA-1 with SHA-256' },
15
+ { id: 'rfx-java-012', match: /Cipher\.getInstance\("DES/g, replace: 'Cipher.getInstance("AES/GCM/NoPadding', lang: 'java', tier: 1, title: 'Replace Java DES with AES-GCM' },
16
+ { id: 'rfx-java-013', match: /Cipher\.getInstance\("AES\/ECB/g, replace: 'Cipher.getInstance("AES/GCM/NoPadding', lang: 'java', tier: 1, title: 'Replace Java AES-ECB with AES-GCM' },
17
+ { id: 'rfx-java-014', match: /new\s+Random\(\)/g, replace: 'new SecureRandom()', lang: 'java', tier: 1, title: 'Replace Java Random with SecureRandom' },
18
+ { id: 'rfx-java-015', match: /java\.util\.Random/g, replace: 'java.security.SecureRandom', lang: 'java', tier: 1, title: 'Import SecureRandom instead of Random' },
19
+ { id: 'rfx-java-016', match: /cookie\.setSecure\(\s*false\s*\)/g, replace: 'cookie.setSecure(true)', lang: 'java', tier: 1, title: 'Enable Java secure cookies' },
20
+ { id: 'rfx-java-017', match: /cookie\.setHttpOnly\(\s*false\s*\)/g, replace: 'cookie.setHttpOnly(true)', lang: 'java', tier: 1, title: 'Enable Java httpOnly cookies' },
21
+ { id: 'rfx-java-018', match: /\.setAllowedOrigins\(\s*Arrays\.asList\(\s*"\*"\s*\)\s*\)/g, replace: '.setAllowedOrigins(Arrays.asList("http://localhost:3000"))', lang: 'java', tier: 1, title: 'Restrict Java CORS origins' },
22
+ { id: 'rfx-java-019', match: /new\s+File\(\s*request\.getParameter/g, replace: '/* SECURITY: path traversal risk */ new File(request.getParameter', lang: 'java', tier: 2, title: 'Flag Java path traversal' },
23
+ { id: 'rfx-java-020', match: /response\.sendRedirect\(\s*request\.getParameter/g, replace: '/* SECURITY: open redirect */ response.sendRedirect(request.getParameter', lang: 'java', tier: 2, title: 'Flag Java open redirect' },
24
+ { id: 'rfx-java-021', match: /InitialDirContext\(\s*[^)]*request\.getParameter/g, replace: (m) => `/* SECURITY: LDAP injection */ ${m}`, lang: 'java', tier: 2, title: 'Flag Java LDAP injection' },
25
+ { id: 'rfx-java-022', match: /System\.out\.println\(.*password/gi, replace: '/* SECURITY: do not print passwords */', lang: 'java', tier: 1, title: 'Remove Java password printing' },
26
+ { id: 'rfx-java-023', match: /System\.out\.println\(.*secret/gi, replace: '/* SECURITY: do not print secrets */', lang: 'java', tier: 1, title: 'Remove Java secret printing' },
27
+ { id: 'rfx-java-024', match: /logger\.\w+\(.*password/gi, replace: (m) => m.replace(/password\w*/gi, '"[REDACTED]"'), lang: 'java', tier: 1, title: 'Redact passwords in Java logs' },
28
+ { id: 'rfx-java-025', match: /password\.equals\(\s*"[^"]+"\s*\)/g, replace: '/* SECURITY: use BCrypt.checkpw() instead of plaintext comparison */', lang: 'java', tier: 2, title: 'Flag Java plaintext password comparison' },
29
+ { id: 'rfx-java-026', match: /e\.printStackTrace\(\)/g, replace: 'logger.error("Error occurred", e)', lang: 'java', tier: 1, title: 'Replace printStackTrace with logger' },
30
+ { id: 'rfx-java-027', match: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/g, replace: (m) => m.replace('{}', '{ logger.error("Unhandled exception", e); }'), lang: 'java', tier: 1, title: 'Handle empty Java catch block' },
31
+ { id: 'rfx-java-028', match: /response\.getWriter\(\)\.write\(\s*exception\.getMessage\(\)/g, replace: 'response.getWriter().write("Internal server error"', lang: 'java', tier: 1, title: 'Hide Java exception message from response' },
32
+ { id: 'rfx-java-029', match: /SSLContext\.getInstance\("SSL"\)/g, replace: 'SSLContext.getInstance("TLSv1.3")', lang: 'java', tier: 1, title: 'Upgrade Java SSL to TLSv1.3' },
33
+ { id: 'rfx-java-030', match: /SSLContext\.getInstance\("TLSv1"\)/g, replace: 'SSLContext.getInstance("TLSv1.3")', lang: 'java', tier: 1, title: 'Upgrade Java TLSv1 to TLSv1.3' },
34
+ { id: 'rfx-java-031', match: /\.setHostnameVerifier\(\s*SSLSocketFactory\.ALLOW_ALL_HOSTNAME_VERIFIER/g, replace: '.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER', lang: 'java', tier: 1, title: 'Enable strict hostname verification' },
35
+ { id: 'rfx-java-032', match: /Pattern\.compile\(\s*request\.getParameter/g, replace: '/* SECURITY: ReDoS risk */ Pattern.compile(Pattern.quote(request.getParameter', lang: 'java', tier: 2, title: 'Escape Java regex user input' },
36
+ { id: 'rfx-java-033', match: /getClass\(\)\.getClassLoader\(\)\.loadClass\(\s*request/g, replace: '/* SECURITY: class loading injection */ getClass().getClassLoader().loadClass(request', lang: 'java', tier: 2, title: 'Flag Java class loading injection' },
37
+ { id: 'rfx-java-034', match: /ScriptEngine.*eval\(\s*request/g, replace: '/* SECURITY: script injection */ ScriptEngine eval(request', lang: 'java', tier: 2, title: 'Flag Java ScriptEngine eval injection' },
38
+ { id: 'rfx-java-035', match: /new\s+FileOutputStream\([^,)]+,\s*false\)/g, replace: (m) => m, lang: 'java', tier: 1, title: 'Confirm FileOutputStream overwrite mode' },
39
+
40
+ // --- C# (36-70) ---
41
+ { id: 'rfx-cs-001', match: /SqlCommand\(\s*"SELECT\s+[^"]*"\s*\+/g, replace: (m) => `/* SECURITY: use SqlParameter */ ${m}`, lang: 'csharp', tier: 2, title: 'Flag C# SQL concatenation' },
42
+ { id: 'rfx-cs-002', match: /SqlCommand\(\s*\$"SELECT/g, replace: (m) => `/* SECURITY: use SqlParameter, not interpolation */ ${m}`, lang: 'csharp', tier: 2, title: 'Flag C# SQL interpolation' },
43
+ { id: 'rfx-cs-003', match: /SqlCommand\(\s*"(?:INSERT|UPDATE|DELETE)\s+[^"]*"\s*\+/g, replace: (m) => `/* SECURITY: use SqlParameter */ ${m}`, lang: 'csharp', tier: 2, title: 'Flag C# SQL mutation concatenation' },
44
+ { id: 'rfx-cs-004', match: /Process\.Start\(\s*"cmd"\s*,\s*"\/c\s*"\s*\+/g, replace: '/* SECURITY: command injection */ Process.Start("cmd", "/c " +', lang: 'csharp', tier: 2, title: 'Flag C# command injection' },
45
+ { id: 'rfx-cs-005', match: /BinaryFormatter\(\)/g, replace: '/* SECURITY: BinaryFormatter is insecure — use JsonSerializer */ new JsonSerializer()', lang: 'csharp', tier: 2, title: 'Replace C# BinaryFormatter' },
46
+ { id: 'rfx-cs-006', match: /SoapFormatter\(\)/g, replace: '/* SECURITY: SoapFormatter is insecure */ new JsonSerializer()', lang: 'csharp', tier: 2, title: 'Replace C# SoapFormatter' },
47
+ { id: 'rfx-cs-007', match: /MD5\.Create\(\)/g, replace: 'SHA256.Create()', lang: 'csharp', tier: 1, title: 'Replace C# MD5 with SHA-256' },
48
+ { id: 'rfx-cs-008', match: /SHA1\.Create\(\)/g, replace: 'SHA256.Create()', lang: 'csharp', tier: 1, title: 'Replace C# SHA-1 with SHA-256' },
49
+ { id: 'rfx-cs-009', match: /new\s+DESCryptoServiceProvider/g, replace: 'Aes.Create()', lang: 'csharp', tier: 1, title: 'Replace C# DES with AES' },
50
+ { id: 'rfx-cs-010', match: /new\s+TripleDESCryptoServiceProvider/g, replace: 'Aes.Create()', lang: 'csharp', tier: 1, title: 'Replace C# 3DES with AES' },
51
+ { id: 'rfx-cs-011', match: /new\s+Random\(\)/g, replace: 'RandomNumberGenerator.Create()', lang: 'csharp', tier: 1, title: 'Replace C# Random with RNG' },
52
+ { id: 'rfx-cs-012', match: /System\.Random/g, replace: 'System.Security.Cryptography.RandomNumberGenerator', lang: 'csharp', tier: 1, title: 'Import RNG instead of Random' },
53
+ { id: 'rfx-cs-013', match: /cookie\.Secure\s*=\s*false/g, replace: 'cookie.Secure = true', lang: 'csharp', tier: 1, title: 'Enable C# secure cookies' },
54
+ { id: 'rfx-cs-014', match: /cookie\.HttpOnly\s*=\s*false/g, replace: 'cookie.HttpOnly = true', lang: 'csharp', tier: 1, title: 'Enable C# httpOnly cookies' },
55
+ { id: 'rfx-cs-015', match: /SameSiteMode\.None/g, replace: 'SameSiteMode.Strict', lang: 'csharp', tier: 1, title: 'Set C# SameSite to Strict' },
56
+ { id: 'rfx-cs-016', match: /HtmlEncode/g, replace: 'HtmlEncode', lang: 'csharp', tier: 1, title: 'Confirm HtmlEncode usage' },
57
+ { id: 'rfx-cs-017', match: /Response\.Write\(\s*Request/g, replace: '/* SECURITY: XSS risk */ Response.Write(HttpUtility.HtmlEncode(Request', lang: 'csharp', tier: 2, title: 'Add HtmlEncode to Response.Write' },
58
+ { id: 'rfx-cs-018', match: /\.AllowAnyOrigin\(\)/g, replace: '.WithOrigins("http://localhost:3000")', lang: 'csharp', tier: 1, title: 'Restrict C# CORS origins' },
59
+ { id: 'rfx-cs-019', match: /Redirect\(\s*Request\[/g, replace: '/* SECURITY: open redirect */ Redirect(Request[', lang: 'csharp', tier: 2, title: 'Flag C# open redirect' },
60
+ { id: 'rfx-cs-020', match: /Path\.Combine\(\s*\w+\s*,\s*Request/g, replace: '/* SECURITY: path traversal */ Path.Combine(basePath, Path.GetFileName(Request', lang: 'csharp', tier: 2, title: 'Fix C# path traversal' },
61
+ { id: 'rfx-cs-021', match: /DirectorySearcher\(\s*".*"\s*\+\s*Request/g, replace: (m) => `/* SECURITY: LDAP injection */ ${m}`, lang: 'csharp', tier: 2, title: 'Flag C# LDAP injection' },
62
+ { id: 'rfx-cs-022', match: /Console\.WriteLine\(.*password/gi, replace: '/* SECURITY: do not print passwords */', lang: 'csharp', tier: 1, title: 'Remove C# password printing' },
63
+ { id: 'rfx-cs-023', match: /Console\.WriteLine\(.*secret/gi, replace: '/* SECURITY: do not print secrets */', lang: 'csharp', tier: 1, title: 'Remove C# secret printing' },
64
+ { id: 'rfx-cs-024', match: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/g, replace: (m) => m.replace('{}', '{ _logger.LogError(ex, "Error"); }'), lang: 'csharp', tier: 1, title: 'Handle empty C# catch block' },
65
+ { id: 'rfx-cs-025', match: /return\s+Content\(\s*ex\.(?:Message|StackTrace)/g, replace: 'return Content("Internal server error"', lang: 'csharp', tier: 1, title: 'Hide C# exception from response' },
66
+ { id: 'rfx-cs-026', match: /SslProtocols\.Ssl3/g, replace: 'SslProtocols.Tls13', lang: 'csharp', tier: 1, title: 'Upgrade C# SSL3 to TLS1.3' },
67
+ { id: 'rfx-cs-027', match: /SslProtocols\.Tls\b/g, replace: 'SslProtocols.Tls13', lang: 'csharp', tier: 1, title: 'Upgrade C# TLS to TLS1.3' },
68
+ { id: 'rfx-cs-028', match: /ServicePointManager\.ServerCertificateValidationCallback\s*=.*true/g, replace: '/* SECURITY: do not bypass cert validation */ ServicePointManager.ServerCertificateValidationCallback = null', lang: 'csharp', tier: 1, title: 'Remove C# cert validation bypass' },
69
+ { id: 'rfx-cs-029', match: /XmlDocument\(\)/g, replace: 'XmlDocument() { XmlResolver = null }', lang: 'csharp', tier: 1, title: 'Disable C# XXE in XmlDocument' },
70
+ { id: 'rfx-cs-030', match: /XmlTextReader\(/g, replace: '/* set DtdProcessing = DtdProcessing.Prohibit */ XmlTextReader(', lang: 'csharp', tier: 2, title: 'Disable C# XXE in XmlTextReader' },
71
+ { id: 'rfx-cs-031', match: /new\s+Regex\(\s*Request/g, replace: '/* SECURITY: ReDoS risk */ new Regex(Regex.Escape(Request', lang: 'csharp', tier: 2, title: 'Escape C# regex user input' },
72
+ { id: 'rfx-cs-032', match: /File\.ReadAllText\(\s*Request/g, replace: '/* SECURITY: path traversal */ File.ReadAllText(Path.GetFullPath(Request', lang: 'csharp', tier: 2, title: 'Flag C# file read with user input' },
73
+ { id: 'rfx-cs-033', match: /password\s*==\s*"[^"]+"/g, replace: '/* SECURITY: use BCrypt.Verify instead of plaintext comparison */', lang: 'csharp', tier: 2, title: 'Flag C# plaintext password comparison' },
74
+ { id: 'rfx-cs-034', match: /\.RequireHttpsMetadata\s*=\s*false/g, replace: '.RequireHttpsMetadata = true', lang: 'csharp', tier: 1, title: 'Enable C# HTTPS metadata requirement' },
75
+ { id: 'rfx-cs-035', match: /ValidateIssuer\s*=\s*false/g, replace: 'ValidateIssuer = true', lang: 'csharp', tier: 1, title: 'Enable C# JWT issuer validation' },
76
+ ];
77
+ export default fixes;