getdoorman 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
package/src/fixer.js
ADDED
|
@@ -0,0 +1,2113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync, writeFileSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { check } from './index.js';
|
|
6
|
+
import { recordFix } from './metrics.js';
|
|
7
|
+
import { applyRegistryFix, applyAllRegistryFixes } from './fix-engine.js';
|
|
8
|
+
import jsFixEntries from './fix-registry-js.js';
|
|
9
|
+
import pyFixEntries from './fix-registry-python.js';
|
|
10
|
+
import goRustFixEntries from './fix-registry-go-rust.js';
|
|
11
|
+
import javaCsharpFixEntries from './fix-registry-java-csharp.js';
|
|
12
|
+
import mcpAiFixEntries from './fix-registry-mcp-ai.js';
|
|
13
|
+
import extraFixEntries from './fix-registry-extra.js';
|
|
14
|
+
|
|
15
|
+
// Merge all registry-based fix entries
|
|
16
|
+
const registryEntries = [
|
|
17
|
+
...jsFixEntries, ...pyFixEntries, ...goRustFixEntries,
|
|
18
|
+
...javaCsharpFixEntries, ...mcpAiFixEntries, ...extraFixEntries,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Build patternFixes entries from registry
|
|
22
|
+
const registryPatternFixes = {};
|
|
23
|
+
for (const entry of registryEntries) {
|
|
24
|
+
registryPatternFixes[entry.id] = {
|
|
25
|
+
fn: (content) => applyRegistryFix(content, entry),
|
|
26
|
+
tier: entry.tier,
|
|
27
|
+
title: entry.title,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_FILE_SIZE = 500 * 1024; // 500KB
|
|
32
|
+
const MAX_FILES_PER_RUN = 50;
|
|
33
|
+
const FREE_FIX_LIMIT = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if the modified content still has valid syntax.
|
|
37
|
+
* Catches broken JS/TS/JSON before writing to disk.
|
|
38
|
+
*/
|
|
39
|
+
function isSyntaxValid(filePath, content) {
|
|
40
|
+
try {
|
|
41
|
+
if (filePath.endsWith('.json')) {
|
|
42
|
+
JSON.parse(content);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (/\.(js|jsx|mjs|cjs)$/.test(filePath)) {
|
|
46
|
+
// Use Node's built-in parser via new Function (catches syntax errors)
|
|
47
|
+
new Function(content);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// For other file types (TS, Python, etc.) — assume valid
|
|
51
|
+
// Full parsing would require language-specific parsers
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the user has a Pro license key.
|
|
60
|
+
* Accepts DOORMAN_KEY env var or --license-key CLI option.
|
|
61
|
+
*/
|
|
62
|
+
export function isProUser(options = {}) {
|
|
63
|
+
const key = options.licenseKey || process.env.DOORMAN_KEY || '';
|
|
64
|
+
return key.length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Pattern-based auto-fix functions (no AI required)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Each fix function takes (content, finding) and returns the modified content
|
|
73
|
+
* string, or null if the fix could not be applied safely.
|
|
74
|
+
*
|
|
75
|
+
* The `finding` object may include: { file, line, ruleId, title, match, ... }
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
// 1. Replace eval(x) with safer alternatives
|
|
79
|
+
export function fixEval(content) {
|
|
80
|
+
let modified = content;
|
|
81
|
+
// Replace eval(JSON.parse(...)) or eval on JSON-like strings with JSON.parse
|
|
82
|
+
modified = modified.replace(
|
|
83
|
+
/\beval\(\s*(['"`])\s*\{[\s\S]*?\}\s*\1\s*\)/g,
|
|
84
|
+
(match) => {
|
|
85
|
+
const inner = match.slice(match.indexOf('(') + 1, -1).trim();
|
|
86
|
+
return `JSON.parse(${inner})`;
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
// Replace eval(variable) with Function constructor + warning comment
|
|
90
|
+
modified = modified.replace(
|
|
91
|
+
/\beval\(([^)]+)\)/g,
|
|
92
|
+
(match, inner) => {
|
|
93
|
+
const trimmed = inner.trim();
|
|
94
|
+
// Don't double-fix if already replaced
|
|
95
|
+
if (match.startsWith('JSON.parse') || match.startsWith('new Function')) return match;
|
|
96
|
+
// If it looks like JSON parsing, use JSON.parse
|
|
97
|
+
if (/json/i.test(trimmed)) {
|
|
98
|
+
return `JSON.parse(${trimmed})`;
|
|
99
|
+
}
|
|
100
|
+
return `/* SECURITY: replaced unsafe eval — review this usage */ new Function(${trimmed})()`;
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
return modified === content ? null : modified;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Replace Math.random() in security context with crypto.randomUUID()
|
|
107
|
+
export function fixMathRandom(content) {
|
|
108
|
+
// Only replace Math.random() that appears near security-related tokens
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
let changed = false;
|
|
111
|
+
const securityKeywords = /token|secret|key|password|auth|session|csrf|nonce|salt|random.*id|id.*random/i;
|
|
112
|
+
|
|
113
|
+
const result = lines.map((line) => {
|
|
114
|
+
if (/Math\.random\(\)/.test(line) && securityKeywords.test(line)) {
|
|
115
|
+
changed = true;
|
|
116
|
+
return line.replace(/Math\.random\(\)/g, 'crypto.randomUUID()');
|
|
117
|
+
}
|
|
118
|
+
return line;
|
|
119
|
+
}).join('\n');
|
|
120
|
+
|
|
121
|
+
if (!changed) {
|
|
122
|
+
// Fallback: replace any Math.random() usage
|
|
123
|
+
const fallback = content.replace(/Math\.random\(\)/g, 'crypto.randomUUID()');
|
|
124
|
+
return fallback === content ? null : fallback;
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. Add httpOnly, secure, sameSite to cookie options
|
|
130
|
+
export function fixCookieFlags(content) {
|
|
131
|
+
let modified = content;
|
|
132
|
+
// Match res.cookie(..., { ... }) and add missing flags
|
|
133
|
+
modified = modified.replace(
|
|
134
|
+
/(res\.cookie\([^,]+,\s*[^,]+,\s*\{)([^}]*)\}/g,
|
|
135
|
+
(match, prefix, options) => {
|
|
136
|
+
const hasHttpOnly = /httpOnly\s*:/.test(options);
|
|
137
|
+
const hasSecure = /\bsecure\s*:/.test(options);
|
|
138
|
+
const hasSameSite = /sameSite\s*:/.test(options);
|
|
139
|
+
// If all flags present, skip
|
|
140
|
+
if (hasHttpOnly && hasSecure && hasSameSite) return match;
|
|
141
|
+
let opts = options;
|
|
142
|
+
if (!hasHttpOnly) {
|
|
143
|
+
opts = opts.trimEnd();
|
|
144
|
+
if (opts.length > 0 && !opts.endsWith(',')) opts += ',';
|
|
145
|
+
opts += ' httpOnly: true,';
|
|
146
|
+
}
|
|
147
|
+
if (!hasSecure) {
|
|
148
|
+
opts = opts.trimEnd();
|
|
149
|
+
if (!opts.endsWith(',')) opts += ',';
|
|
150
|
+
opts += ' secure: true,';
|
|
151
|
+
}
|
|
152
|
+
if (!hasSameSite) {
|
|
153
|
+
opts = opts.trimEnd();
|
|
154
|
+
if (!opts.endsWith(',')) opts += ',';
|
|
155
|
+
opts += " sameSite: 'strict'";
|
|
156
|
+
}
|
|
157
|
+
return `${prefix}${opts} }`;
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
// Handle res.cookie with only 2 args (no options) — add options object
|
|
161
|
+
modified = modified.replace(
|
|
162
|
+
/(res\.cookie\(\s*[^,]+,\s*[^,)]+)\s*\)/g,
|
|
163
|
+
(match, prefix) => {
|
|
164
|
+
// Avoid matching if already has 3+ args
|
|
165
|
+
if (prefix.split(',').length > 2) return match;
|
|
166
|
+
return `${prefix}, { httpOnly: true, secure: true, sameSite: 'strict' })`;
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
return modified === content ? null : modified;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 4. Replace http:// URLs with https:// in config files
|
|
173
|
+
export function fixHttpUrls(content) {
|
|
174
|
+
const modified = content.replace(
|
|
175
|
+
/(['"`])http:\/\/((?!localhost|127\.0\.0\.1|0\.0\.0\.0|::1)[^'"`\s]+)(['"`])/g,
|
|
176
|
+
(match, q1, url, q2) => `${q1}https://${url}${q2}`,
|
|
177
|
+
);
|
|
178
|
+
return modified === content ? null : modified;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 5. Add helmet() import and middleware to Express apps
|
|
182
|
+
export function fixHelmetMissing(content) {
|
|
183
|
+
// Only apply if this looks like an Express app without helmet
|
|
184
|
+
if (!/express\(\)/.test(content)) return null;
|
|
185
|
+
if (/helmet/.test(content)) return null;
|
|
186
|
+
|
|
187
|
+
let modified = content;
|
|
188
|
+
// Add import after the express import
|
|
189
|
+
modified = modified.replace(
|
|
190
|
+
/((?:const|let|var)\s+\w+\s*=\s*require\s*\(\s*['"]express['"]\s*\).*?;?\n)/,
|
|
191
|
+
`$1const helmet = require('helmet');\n`,
|
|
192
|
+
);
|
|
193
|
+
// If using ES module imports
|
|
194
|
+
modified = modified.replace(
|
|
195
|
+
/(import\s+\w+\s+from\s+['"]express['"].*?\n)/,
|
|
196
|
+
`$1import helmet from 'helmet';\n`,
|
|
197
|
+
);
|
|
198
|
+
// Add app.use(helmet()) after app creation
|
|
199
|
+
modified = modified.replace(
|
|
200
|
+
/((?:const|let|var)\s+(\w+)\s*=\s*express\(\).*?;?\n)/,
|
|
201
|
+
`$1$2.use(helmet());\n`,
|
|
202
|
+
);
|
|
203
|
+
return modified === content ? null : modified;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 6. Replace == with === in auth comparisons
|
|
207
|
+
export function fixLooseEquality(content) {
|
|
208
|
+
const authKeywords = /password|token|secret|auth|session|user|role|admin|login|credential|apiKey|api_key/i;
|
|
209
|
+
const lines = content.split('\n');
|
|
210
|
+
let changed = false;
|
|
211
|
+
|
|
212
|
+
const result = lines.map((line) => {
|
|
213
|
+
// Skip comments
|
|
214
|
+
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) return line;
|
|
215
|
+
if (authKeywords.test(line) && /[^!=]==[^=]/.test(line)) {
|
|
216
|
+
changed = true;
|
|
217
|
+
return line.replace(/([^!=])={2}([^=])/g, '$1===$2');
|
|
218
|
+
}
|
|
219
|
+
if (authKeywords.test(line) && /[^!]=![^=]/.test(line)) {
|
|
220
|
+
// skip — != to !== is rarer, handle == only
|
|
221
|
+
}
|
|
222
|
+
return line;
|
|
223
|
+
}).join('\n');
|
|
224
|
+
|
|
225
|
+
return changed ? result : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 7. Add algorithms option to jwt.verify calls
|
|
229
|
+
export function fixJwtVerifyAlgorithm(content) {
|
|
230
|
+
const modified = content.replace(
|
|
231
|
+
/jwt\.verify\(\s*([^,]+),\s*([^,)]+)\s*\)/g,
|
|
232
|
+
(match, token, secret) => {
|
|
233
|
+
return `jwt.verify(${token}, ${secret}, { algorithms: ['HS256'] })`;
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
return modified === content ? null : modified;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 8. Replace MD5/SHA1 with SHA256 in crypto.createHash
|
|
240
|
+
export function fixWeakHash(content) {
|
|
241
|
+
const modified = content.replace(
|
|
242
|
+
/crypto\.createHash\(\s*['"](?:md5|sha1|MD5|SHA1)['"]\s*\)/g,
|
|
243
|
+
"crypto.createHash('sha256')",
|
|
244
|
+
);
|
|
245
|
+
return modified === content ? null : modified;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 9. Add -- comment terminator to SQL queries with string interpolation
|
|
249
|
+
export function fixSqlTerminator(content) {
|
|
250
|
+
// Match template literals and string concat that look like SQL with interpolation
|
|
251
|
+
const modified = content.replace(
|
|
252
|
+
/(`\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\b[^`]*\$\{[^}]+\}[^`]*?)(`)/gi,
|
|
253
|
+
(match, query, close) => {
|
|
254
|
+
if (/--\s*$/.test(query.trim())) return match; // already has terminator
|
|
255
|
+
return `${query} --${close}`;
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
return modified === content ? null : modified;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 10. Replace child_process.exec with execFile
|
|
262
|
+
export function fixExecToExecFile(content) {
|
|
263
|
+
let modified = content;
|
|
264
|
+
// Replace require destructuring
|
|
265
|
+
modified = modified.replace(
|
|
266
|
+
/\b(require\(\s*['"]child_process['"]\s*\))\.exec\b/g,
|
|
267
|
+
'$1.execFile',
|
|
268
|
+
);
|
|
269
|
+
// Replace destructured import: { exec } -> { execFile }
|
|
270
|
+
modified = modified.replace(
|
|
271
|
+
/\{\s*exec\s*\}(\s*=\s*require\(\s*['"]child_process['"]\s*\))/g,
|
|
272
|
+
'{ execFile }$1',
|
|
273
|
+
);
|
|
274
|
+
// Replace ES module import
|
|
275
|
+
modified = modified.replace(
|
|
276
|
+
/import\s*\{\s*exec\s*\}\s*from\s*['"]child_process['"]/g,
|
|
277
|
+
"import { execFile } from 'child_process'",
|
|
278
|
+
);
|
|
279
|
+
// Replace direct exec( calls with execFile( — only when clearly from child_process
|
|
280
|
+
modified = modified.replace(
|
|
281
|
+
/\bexec\(\s*(['"`])/g,
|
|
282
|
+
(match, quote) => `execFile(${quote}`,
|
|
283
|
+
);
|
|
284
|
+
return modified === content ? null : modified;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 11. Add expiresIn to jwt.sign calls missing expiration
|
|
288
|
+
export function fixJwtSignExpiry(content) {
|
|
289
|
+
// Match jwt.sign(payload, secret) without options or with options missing expiresIn
|
|
290
|
+
let modified = content;
|
|
291
|
+
|
|
292
|
+
// Case 1: jwt.sign(payload, secret) — no options at all
|
|
293
|
+
modified = modified.replace(
|
|
294
|
+
/jwt\.sign\(\s*([^,]+),\s*([^,)]+)\s*\)(?!\s*;?\s*\/\*.*expiresIn)/g,
|
|
295
|
+
(match, payload, secret) => {
|
|
296
|
+
// Make sure this isn't already a 3-arg call
|
|
297
|
+
if (secret.includes('{')) return match;
|
|
298
|
+
return `jwt.sign(${payload}, ${secret}, { expiresIn: '1h' })`;
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Case 2: jwt.sign(payload, secret, { ... }) where options exist but no expiresIn
|
|
303
|
+
modified = modified.replace(
|
|
304
|
+
/jwt\.sign\(\s*([^,]+),\s*([^,]+),\s*\{([^}]*)\}\s*\)/g,
|
|
305
|
+
(match, payload, secret, options) => {
|
|
306
|
+
if (/expiresIn/.test(options)) return match;
|
|
307
|
+
let opts = options.trimEnd();
|
|
308
|
+
if (opts.length > 0 && !opts.endsWith(',')) opts += ',';
|
|
309
|
+
return `jwt.sign(${payload}, ${secret}, {${opts} expiresIn: '1h' })`;
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return modified === content ? null : modified;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 12. Add rate-limit middleware to Express apps
|
|
317
|
+
export function fixRateLimitMissing(content) {
|
|
318
|
+
if (!/express\(\)/.test(content)) return null;
|
|
319
|
+
if (/rate.?limit/i.test(content)) return null;
|
|
320
|
+
|
|
321
|
+
let modified = content;
|
|
322
|
+
// Add require after express require
|
|
323
|
+
modified = modified.replace(
|
|
324
|
+
/((?:const|let|var)\s+\w+\s*=\s*require\s*\(\s*['"]express['"]\s*\).*?;?\n)/,
|
|
325
|
+
`$1const rateLimit = require('express-rate-limit');\n`,
|
|
326
|
+
);
|
|
327
|
+
// ES module import
|
|
328
|
+
modified = modified.replace(
|
|
329
|
+
/(import\s+\w+\s+from\s+['"]express['"].*?\n)/,
|
|
330
|
+
`$1import rateLimit from 'express-rate-limit';\n`,
|
|
331
|
+
);
|
|
332
|
+
// Add middleware after app creation
|
|
333
|
+
modified = modified.replace(
|
|
334
|
+
/((?:const|let|var)\s+(\w+)\s*=\s*express\(\).*?;?\n)/,
|
|
335
|
+
`$1$2.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));\n`,
|
|
336
|
+
);
|
|
337
|
+
return modified === content ? null : modified;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 13. Replace JSON.parse(req.body) with body-parser middleware
|
|
341
|
+
export function fixJsonParseReqBody(content) {
|
|
342
|
+
let modified = content;
|
|
343
|
+
// Replace JSON.parse(req.body) with just req.body (assumes body-parser)
|
|
344
|
+
modified = modified.replace(
|
|
345
|
+
/JSON\.parse\(\s*req\.body\s*\)/g,
|
|
346
|
+
'req.body /* use express.json() middleware instead of manual JSON.parse */',
|
|
347
|
+
);
|
|
348
|
+
// If this is an Express app, add express.json() middleware
|
|
349
|
+
if (/express\(\)/.test(modified) && !/\.use\(\s*express\.json\(\)\s*\)/.test(modified) && !/bodyParser/.test(modified)) {
|
|
350
|
+
modified = modified.replace(
|
|
351
|
+
/((?:const|let|var)\s+(\w+)\s*=\s*express\(\).*?;?\n)/,
|
|
352
|
+
`$1$2.use(express.json());\n`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return modified === content ? null : modified;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 14. Add .escape() to user input before SQL (stopgap)
|
|
359
|
+
export function fixSqlEscape(content) {
|
|
360
|
+
// Look for direct string interpolation in SQL-like queries
|
|
361
|
+
const modified = content.replace(
|
|
362
|
+
/(`\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^`]*)\$\{(\s*(?:req\.(?:body|query|params)\.\w+|userInput|input)\s*)\}([^`]*`)/gi,
|
|
363
|
+
(match, before, variable, after) => {
|
|
364
|
+
const trimmedVar = variable.trim();
|
|
365
|
+
// Don't double-wrap
|
|
366
|
+
if (trimmedVar.includes('escape(') || trimmedVar.includes('sanitize(')) return match;
|
|
367
|
+
return `${before}\${escape(${trimmedVar})}${after}`;
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
return modified === content ? null : modified;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 15. Add Content-Security-Policy header via helmet
|
|
374
|
+
export function fixContentSecurityPolicy(content) {
|
|
375
|
+
// If helmet is already used with contentSecurityPolicy, skip
|
|
376
|
+
if (/contentSecurityPolicy/.test(content)) return null;
|
|
377
|
+
if (!/express\(\)/.test(content)) return null;
|
|
378
|
+
|
|
379
|
+
let modified = content;
|
|
380
|
+
// If helmet() is already used, upgrade to include CSP directives
|
|
381
|
+
if (/helmet\(\)/.test(modified)) {
|
|
382
|
+
modified = modified.replace(
|
|
383
|
+
/helmet\(\)/,
|
|
384
|
+
`helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"] } } })`,
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
// Add helmet with CSP
|
|
388
|
+
modified = modified.replace(
|
|
389
|
+
/((?:const|let|var)\s+(\w+)\s*=\s*express\(\).*?;?\n)/,
|
|
390
|
+
`$1$2.use(require('helmet')({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"] } } }));\n`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return modified === content ? null : modified;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 16. Remove console.log statements (replace with logger)
|
|
397
|
+
export function fixConsoleLog(content) {
|
|
398
|
+
const lines = content.split('\n');
|
|
399
|
+
let changed = false;
|
|
400
|
+
|
|
401
|
+
const result = lines.map((line) => {
|
|
402
|
+
// Skip comments
|
|
403
|
+
if (/^\s*\/\//.test(line)) return line;
|
|
404
|
+
// Replace console.log/debug/info/warn/error with logger equivalents
|
|
405
|
+
if (/\bconsole\.(log|debug|info)\s*\(/.test(line)) {
|
|
406
|
+
changed = true;
|
|
407
|
+
return line
|
|
408
|
+
.replace(/\bconsole\.log\s*\(/, 'logger.info(')
|
|
409
|
+
.replace(/\bconsole\.debug\s*\(/, 'logger.debug(')
|
|
410
|
+
.replace(/\bconsole\.info\s*\(/, 'logger.info(');
|
|
411
|
+
}
|
|
412
|
+
return line;
|
|
413
|
+
}).join('\n');
|
|
414
|
+
|
|
415
|
+
return changed ? result : null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 17. Add return type to exported TypeScript functions
|
|
419
|
+
export function fixTsReturnType(content) {
|
|
420
|
+
const modified = content.replace(
|
|
421
|
+
/^(export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\))\s*\{/gm,
|
|
422
|
+
(match, sig) => {
|
|
423
|
+
// Skip if already has return type annotation
|
|
424
|
+
if (/\)\s*:\s*\S/.test(sig)) return match;
|
|
425
|
+
return `${sig}: void {`;
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
return modified === content ? null : modified;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 18. Convert var to const/let
|
|
432
|
+
export function fixVarToConstLet(content) {
|
|
433
|
+
const lines = content.split('\n');
|
|
434
|
+
let changed = false;
|
|
435
|
+
|
|
436
|
+
const result = lines.map((line) => {
|
|
437
|
+
// Skip comments
|
|
438
|
+
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) return line;
|
|
439
|
+
// Replace var with const for assignments that look like constants
|
|
440
|
+
const varMatch = line.match(/^(\s*)var\s+(\w+)\s*=/);
|
|
441
|
+
if (varMatch) {
|
|
442
|
+
changed = true;
|
|
443
|
+
// Use const by default (safer); let if reassigned later is a manual step
|
|
444
|
+
return line.replace(/\bvar\s+/, 'const ');
|
|
445
|
+
}
|
|
446
|
+
// var without assignment (declaration only) => let
|
|
447
|
+
const varDeclMatch = line.match(/^(\s*)var\s+(\w+)\s*;/);
|
|
448
|
+
if (varDeclMatch) {
|
|
449
|
+
changed = true;
|
|
450
|
+
return line.replace(/\bvar\s+/, 'let ');
|
|
451
|
+
}
|
|
452
|
+
return line;
|
|
453
|
+
}).join('\n');
|
|
454
|
+
|
|
455
|
+
return changed ? result : null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 19. Add error parameter to empty catch blocks
|
|
459
|
+
export function fixEmptyCatch(content) {
|
|
460
|
+
const modified = content.replace(
|
|
461
|
+
/catch\s*\(\s*\)\s*\{/g,
|
|
462
|
+
'catch (error) {',
|
|
463
|
+
);
|
|
464
|
+
return modified === content ? null : modified;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 20. Convert callback-style to async/await (fs.readFile as common case)
|
|
468
|
+
export function fixCallbackToAsync(content) {
|
|
469
|
+
// Convert fs.readFile(path, encoding, (err, data) => { ... }) to await
|
|
470
|
+
const modified = content.replace(
|
|
471
|
+
/fs\.readFile\(\s*([^,]+),\s*(['"][^'"]+['"])\s*,\s*(?:function\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)|(?:\(\s*(\w+)\s*,\s*(\w+)\s*\)|\(\s*(\w+)\s*,\s*(\w+)\s*\))\s*=>)\s*\{/g,
|
|
472
|
+
(match, path, encoding, err1, data1, err2, data2, err3, data3) => {
|
|
473
|
+
const dataVar = data1 || data2 || data3 || 'data';
|
|
474
|
+
return `const ${dataVar} = await fs.promises.readFile(${path}, ${encoding});\n{`;
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
return modified === content ? null : modified;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Advanced JS/TS auto-fix functions (21-40)
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
// 21. Replace dangerouslySetInnerHTML={{__html: var}} with DOMPurify.sanitize wrapper
|
|
485
|
+
export function fixDangerouslySetInnerHTML(content) {
|
|
486
|
+
const modified = content.replace(
|
|
487
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([^}]+)\}\s*\}/g,
|
|
488
|
+
(match, variable) => {
|
|
489
|
+
const trimmed = variable.trim();
|
|
490
|
+
if (/DOMPurify\.sanitize/.test(trimmed)) return match;
|
|
491
|
+
return `dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(${trimmed}) }}`;
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
return modified === content ? null : modified;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 22. Replace document.write(var) with textContent assignment
|
|
498
|
+
export function fixDocumentWrite(content) {
|
|
499
|
+
const modified = content.replace(
|
|
500
|
+
/document\.write\(([^)]+)\)/g,
|
|
501
|
+
(match, arg) => {
|
|
502
|
+
const trimmed = arg.trim();
|
|
503
|
+
return `document.body.textContent = ${trimmed} /* SECURITY: replaced unsafe document.write */`;
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
return modified === content ? null : modified;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 23. Replace el.innerHTML = var with el.textContent = var
|
|
510
|
+
export function fixInnerHTML(content) {
|
|
511
|
+
const modified = content.replace(
|
|
512
|
+
/(\w+(?:\.\w+)*)\.innerHTML\s*=\s*([^;]+);/g,
|
|
513
|
+
(match, el, value) => {
|
|
514
|
+
const trimmed = value.trim();
|
|
515
|
+
if (/DOMPurify|sanitize|escapeHtml/.test(trimmed)) return match;
|
|
516
|
+
return `${el}.textContent = ${trimmed};`;
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
return modified === content ? null : modified;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 24. Replace {$where: userInput} with safe MongoDB query
|
|
523
|
+
export function fixNoSqlInjection(content) {
|
|
524
|
+
const modified = content.replace(
|
|
525
|
+
/\{\s*\$where\s*:\s*([^}]+)\}/g,
|
|
526
|
+
(match, expr) => {
|
|
527
|
+
const trimmed = expr.trim();
|
|
528
|
+
return `{ /* SECURITY: replaced unsafe $where */ $expr: { $eq: [${trimmed}, true] } }`;
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
return modified === content ? null : modified;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 25. Add null prototype to object merge to prevent prototype pollution
|
|
535
|
+
export function fixPrototypePollution(content) {
|
|
536
|
+
let modified = content;
|
|
537
|
+
modified = modified.replace(
|
|
538
|
+
/Object\.assign\(\s*\{\s*\}\s*,\s*([^)]+)\)/g,
|
|
539
|
+
(match, args) => {
|
|
540
|
+
if (/Object\.create\(null\)/.test(match)) return match;
|
|
541
|
+
return `Object.assign(Object.create(null), ${args})`;
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
return modified === content ? null : modified;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 26. Replace string === comparison for secrets with crypto.timingSafeEqual
|
|
548
|
+
export function fixTimingAttack(content) {
|
|
549
|
+
const secretKeywords = /secret|token|apiKey|api_key|password|hash|signature|hmac|digest/i;
|
|
550
|
+
const lines = content.split('\n');
|
|
551
|
+
let changed = false;
|
|
552
|
+
|
|
553
|
+
const result = lines.map((line) => {
|
|
554
|
+
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) return line;
|
|
555
|
+
if (secretKeywords.test(line) && /===/.test(line) && !/timingSafeEqual/.test(line)) {
|
|
556
|
+
const m = line.match(/(\w+)\s*===\s*(\w+)/);
|
|
557
|
+
if (m) {
|
|
558
|
+
changed = true;
|
|
559
|
+
return line.replace(
|
|
560
|
+
/(\w+)\s*===\s*(\w+)/,
|
|
561
|
+
`crypto.timingSafeEqual(Buffer.from(${m[1]}), Buffer.from(${m[2]}))`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return line;
|
|
566
|
+
}).join('\n');
|
|
567
|
+
|
|
568
|
+
return changed ? result : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 27. Add path.resolve + startsWith check around fs operations with user input
|
|
572
|
+
export function fixPathTraversal(content) {
|
|
573
|
+
const modified = content.replace(
|
|
574
|
+
/(fs\.(?:readFileSync|readFile|writeFileSync|writeFile|createReadStream|createWriteStream)\(\s*)(req\.(?:params|query|body)\.\w+|userPath|filePath|inputPath)/g,
|
|
575
|
+
(match, fsCall, pathVar) => {
|
|
576
|
+
return `/* SECURITY: path traversal guard */\n const safePath = path.resolve(__dirname, ${pathVar});\n if (!safePath.startsWith(path.resolve(__dirname))) throw new Error('Path traversal detected');\n ${fsCall}safePath`;
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
return modified === content ? null : modified;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 28. Add safe-regex wrapper to new RegExp(userInput)
|
|
583
|
+
export function fixRegexDos(content) {
|
|
584
|
+
const modified = content.replace(
|
|
585
|
+
/new RegExp\((\s*(?:req\.(?:body|query|params)\.\w+|userInput|userPattern|pattern|input)\s*)\)/g,
|
|
586
|
+
(match, variable) => {
|
|
587
|
+
const trimmed = variable.trim();
|
|
588
|
+
return `/* SECURITY: sanitize regex input to prevent ReDoS */ new RegExp(${trimmed}.replace(/[.*+?^$\{\}()|[\\]\\\\]/g, '\\\\$&'))`;
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
return modified === content ? null : modified;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 29. Add URL validation/whitelist before res.redirect(userInput)
|
|
595
|
+
export function fixOpenRedirect(content) {
|
|
596
|
+
const modified = content.replace(
|
|
597
|
+
/res\.redirect\(\s*(req\.(?:query|body|params)\.\w+|redirectUrl|returnUrl)\s*\)/g,
|
|
598
|
+
(match, variable) => {
|
|
599
|
+
return `/* SECURITY: validate redirect URL to prevent open redirect */\n const allowedHosts = [process.env.APP_HOST || 'localhost'];\n const redirectTarget = new URL(${variable}, \`https://\${allowedHosts[0]}\`);\n if (!allowedHosts.includes(redirectTarget.hostname)) { return res.redirect('/'); }\n res.redirect(redirectTarget.pathname)`;
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
return modified === content ? null : modified;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 30. Add express-rate-limit to route handlers
|
|
606
|
+
export function fixNoRateLimit(content) {
|
|
607
|
+
if (!/\.(get|post|put|delete|patch)\s*\(/.test(content)) return null;
|
|
608
|
+
if (/rateLimit|rate.?limit/i.test(content)) return null;
|
|
609
|
+
if (!/express|router|app/.test(content)) return null;
|
|
610
|
+
|
|
611
|
+
let modified = content;
|
|
612
|
+
if (/require/.test(modified)) {
|
|
613
|
+
modified = `const rateLimit = require('express-rate-limit');\nconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });\n${modified}`;
|
|
614
|
+
} else {
|
|
615
|
+
modified = `import rateLimit from 'express-rate-limit';\nconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });\n${modified}`;
|
|
616
|
+
}
|
|
617
|
+
return modified === content ? null : modified;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 31. Replace req.query.x in response with escaped version
|
|
621
|
+
export function fixXssQueryParam(content) {
|
|
622
|
+
const modified = content.replace(
|
|
623
|
+
/(res\.(?:send|write|end)\(\s*)(req\.query\.\w+)(\s*\))/g,
|
|
624
|
+
(match, prefix, param, suffix) => {
|
|
625
|
+
return `${prefix}String(${param}).replace(/[<>&"']/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' })[c])${suffix}`;
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
return modified === content ? null : modified;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 32. Replace hardcoded 0.0.0.0 with process.env.HOST
|
|
632
|
+
export function fixHardcodedIp(content) {
|
|
633
|
+
const modified = content.replace(
|
|
634
|
+
/(['"])0\.0\.0\.0\1/g,
|
|
635
|
+
"process.env.HOST || '127.0.0.1'",
|
|
636
|
+
);
|
|
637
|
+
return modified === content ? null : modified;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 33. Add CSP directives to existing helmet() call
|
|
641
|
+
export function fixNoHelmetCsp(content) {
|
|
642
|
+
if (!/helmet\(\s*\)/.test(content)) return null;
|
|
643
|
+
if (/contentSecurityPolicy/.test(content)) return null;
|
|
644
|
+
|
|
645
|
+
const modified = content.replace(
|
|
646
|
+
/helmet\(\s*\)/g,
|
|
647
|
+
`helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], objectSrc: ["'none'"], upgradeInsecureRequests: [] } } })`,
|
|
648
|
+
);
|
|
649
|
+
return modified === content ? null : modified;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 34. Replace string concat SQL with parameterized query
|
|
653
|
+
export function fixSqlConcat(content) {
|
|
654
|
+
const modified = content.replace(
|
|
655
|
+
/(['"])(\s*SELECT\s+.+?FROM\s+\w+\s+WHERE\s+\w+\s*=\s*)['"]\s*\+\s*(\w+)/gi,
|
|
656
|
+
(match, quote, sqlPart, variable) => {
|
|
657
|
+
return `${quote}${sqlPart}?${quote}, [${variable}]`;
|
|
658
|
+
},
|
|
659
|
+
);
|
|
660
|
+
return modified === content ? null : modified;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 35. Replace localStorage JWT storage with httpOnly cookie
|
|
664
|
+
export function fixJwtStorageLocal(content) {
|
|
665
|
+
const modified = content.replace(
|
|
666
|
+
/localStorage\.setItem\(\s*(['"])(?:jwt|token|accessToken|access_token|authToken|auth_token)\1\s*,\s*([^)]+)\)/g,
|
|
667
|
+
(match, quote, tokenVar) => {
|
|
668
|
+
return `/* SECURITY: Do NOT store JWTs in localStorage - use httpOnly cookies instead */\n // localStorage.setItem(${quote}token${quote}, ${tokenVar});\n document.cookie = 'token=' + ${tokenVar} + '; Secure; SameSite=Strict; path=/'`;
|
|
669
|
+
},
|
|
670
|
+
);
|
|
671
|
+
return modified === content ? null : modified;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 36. Replace origin: '*' with specific origins array
|
|
675
|
+
export function fixCorsWildcard(content) {
|
|
676
|
+
const modified = content.replace(
|
|
677
|
+
/origin\s*:\s*(['"])\*\1/g,
|
|
678
|
+
"origin: [process.env.ALLOWED_ORIGIN || 'https://yourdomain.com']",
|
|
679
|
+
);
|
|
680
|
+
return modified === content ? null : modified;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 37. Add basic input validation stub for req.body
|
|
684
|
+
export function fixNoInputValidation(content) {
|
|
685
|
+
if (!/req\.body/.test(content)) return null;
|
|
686
|
+
if (/validate|schema|zod|joi|yup|ajv/i.test(content)) return null;
|
|
687
|
+
if (!/express|router|app/.test(content)) return null;
|
|
688
|
+
|
|
689
|
+
let modified = content;
|
|
690
|
+
modified = modified.replace(
|
|
691
|
+
/((?:app|router)\.(?:post|put|patch)\([^)]+,\s*(?:async\s+)?(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{[^}]*)(req\.body)/,
|
|
692
|
+
(match, before, reqBody) => {
|
|
693
|
+
return `${before}/* TODO: Add input validation, e.g.:\n const schema = Joi.object({ name: Joi.string().required() });\n const { error, value } = schema.validate(req.body);\n if (error) return res.status(400).json({ error: error.message });\n */\n ${reqBody}`;
|
|
694
|
+
},
|
|
695
|
+
);
|
|
696
|
+
return modified === content ? null : modified;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 38. Replace console.error in catch with proper logger
|
|
700
|
+
export function fixConsoleError(content) {
|
|
701
|
+
const lines = content.split('\n');
|
|
702
|
+
let changed = false;
|
|
703
|
+
|
|
704
|
+
const result = lines.map((line) => {
|
|
705
|
+
if (/^\s*\/\//.test(line)) return line;
|
|
706
|
+
if (/\bconsole\.error\s*\(/.test(line)) {
|
|
707
|
+
changed = true;
|
|
708
|
+
return line.replace(/\bconsole\.error\s*\(/, 'logger.error(');
|
|
709
|
+
}
|
|
710
|
+
return line;
|
|
711
|
+
}).join('\n');
|
|
712
|
+
|
|
713
|
+
return changed ? result : null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 39. Replace await-in-loop with Promise.all pattern
|
|
717
|
+
export function fixAwaitInLoop(content) {
|
|
718
|
+
const modified = content.replace(
|
|
719
|
+
/for\s*\(\s*(?:const|let|var)\s+(\w+)\s+of\s+(\w+)\s*\)\s*\{([^}]*await\s+(\w+)\(([^)]*)\)[^}]*)\}/gs,
|
|
720
|
+
(match, item, collection, body) => {
|
|
721
|
+
if ((body.match(/await/g) || []).length > 1) return match;
|
|
722
|
+
return `/* PERF: parallelized sequential awaits */\nawait Promise.all(${collection}.map(async (${item}) => {\n${body}\n}))`;
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
return modified === content ? null : modified;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// 40. Add Express error handler middleware at end of app
|
|
729
|
+
export function fixMissingErrorHandler(content) {
|
|
730
|
+
if (!/express\(\)/.test(content)) return null;
|
|
731
|
+
if (/err\s*,\s*req\s*,\s*res\s*,\s*next/.test(content)) return null;
|
|
732
|
+
|
|
733
|
+
let modified = content;
|
|
734
|
+
const errorHandler = `\n// SECURITY: Global error handler - do not leak stack traces\napp.use((err, req, res, next) => {\n logger.error(err.message);\n res.status(err.status || 500).json({ error: 'Internal server error' });\n});\n`;
|
|
735
|
+
|
|
736
|
+
if (/module\.exports/.test(modified)) {
|
|
737
|
+
modified = modified.replace(
|
|
738
|
+
/(module\.exports)/,
|
|
739
|
+
`${errorHandler}\n$1`,
|
|
740
|
+
);
|
|
741
|
+
} else if (/export\s+default/.test(modified)) {
|
|
742
|
+
modified = modified.replace(
|
|
743
|
+
/(export\s+default)/,
|
|
744
|
+
`${errorHandler}\n$1`,
|
|
745
|
+
);
|
|
746
|
+
} else {
|
|
747
|
+
modified = modified + errorHandler;
|
|
748
|
+
}
|
|
749
|
+
return modified === content ? null : modified;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// Python auto-fix functions
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
// P1. Replace DEBUG = True with env-based config
|
|
757
|
+
export function fixPythonDebugTrue(content) {
|
|
758
|
+
const modified = content.replace(
|
|
759
|
+
/^(\s*)DEBUG\s*=\s*True\s*$/gm,
|
|
760
|
+
`$1DEBUG = os.environ.get('DEBUG', 'False') == 'True'`,
|
|
761
|
+
);
|
|
762
|
+
return modified === content ? null : modified;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// P2. Replace hardcoded SECRET_KEY with env variable
|
|
766
|
+
export function fixPythonHardcodedSecret(content) {
|
|
767
|
+
const modified = content.replace(
|
|
768
|
+
/^(\s*)SECRET_KEY\s*=\s*(['"])(?!os\.environ)[^'"]+\2\s*$/gm,
|
|
769
|
+
`$1SECRET_KEY = os.environ['SECRET_KEY']`,
|
|
770
|
+
);
|
|
771
|
+
return modified === content ? null : modified;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// P3. Replace subprocess.call(cmd, shell=True) with shlex.split
|
|
775
|
+
export function fixPythonShellTrue(content) {
|
|
776
|
+
const modified = content.replace(
|
|
777
|
+
/subprocess\.call\((\w+),\s*shell\s*=\s*True\)/g,
|
|
778
|
+
'subprocess.call(shlex.split($1))',
|
|
779
|
+
);
|
|
780
|
+
return modified === content ? null : modified;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// P4. Replace eval(user_input) with ast.literal_eval
|
|
784
|
+
export function fixPythonEval(content) {
|
|
785
|
+
const modified = content.replace(
|
|
786
|
+
/\beval\((\w+)\)/g,
|
|
787
|
+
'ast.literal_eval($1)',
|
|
788
|
+
);
|
|
789
|
+
return modified === content ? null : modified;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// P5. Replace pickle.loads(data) with json.loads(data)
|
|
793
|
+
export function fixPythonPickleLoad(content) {
|
|
794
|
+
const modified = content.replace(
|
|
795
|
+
/pickle\.loads\((\w+)\)/g,
|
|
796
|
+
'json.loads($1) # SECURITY: replaced unsafe pickle.loads with json.loads',
|
|
797
|
+
);
|
|
798
|
+
return modified === content ? null : modified;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// P6. Replace yaml.load(data) with yaml.safe_load(data)
|
|
802
|
+
export function fixPythonYamlLoad(content) {
|
|
803
|
+
const modified = content.replace(
|
|
804
|
+
/yaml\.load\(([^)]+)\)/g,
|
|
805
|
+
(match, args) => {
|
|
806
|
+
// Don't replace if already safe_load
|
|
807
|
+
if (match.includes('safe_load')) return match;
|
|
808
|
+
return `yaml.safe_load(${args})`;
|
|
809
|
+
},
|
|
810
|
+
);
|
|
811
|
+
return modified === content ? null : modified;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// P7. Replace hashlib.md5( with hashlib.sha256(
|
|
815
|
+
export function fixPythonMd5(content) {
|
|
816
|
+
const modified = content.replace(
|
|
817
|
+
/hashlib\.md5\(/g,
|
|
818
|
+
'hashlib.sha256(',
|
|
819
|
+
);
|
|
820
|
+
return modified === content ? null : modified;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// P8. Replace hashlib.sha1( with hashlib.sha256(
|
|
824
|
+
export function fixPythonSha1(content) {
|
|
825
|
+
const modified = content.replace(
|
|
826
|
+
/hashlib\.sha1\(/g,
|
|
827
|
+
'hashlib.sha256(',
|
|
828
|
+
);
|
|
829
|
+
return modified === content ? null : modified;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// P9. Replace verify=False with verify=True in requests calls
|
|
833
|
+
export function fixPythonRequestsNoVerify(content) {
|
|
834
|
+
const modified = content.replace(
|
|
835
|
+
/(requests\.(?:get|post|put|patch|delete|head|options)\([^)]*)\bverify\s*=\s*False/g,
|
|
836
|
+
'$1verify=True',
|
|
837
|
+
);
|
|
838
|
+
return modified === content ? null : modified;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// P10. Add timeout=30 to requests.get/post calls missing timeout
|
|
842
|
+
export function fixPythonRequestsNoTimeout(content) {
|
|
843
|
+
const modified = content.replace(
|
|
844
|
+
/(requests\.(?:get|post|put|patch|delete)\([^)]*)\)(?!.*timeout)/g,
|
|
845
|
+
(match, prefix) => {
|
|
846
|
+
if (/timeout/.test(match)) return match;
|
|
847
|
+
// Check if there are existing kwargs
|
|
848
|
+
const hasArgs = prefix.includes(',');
|
|
849
|
+
return hasArgs ? `${prefix}, timeout=30)` : `${prefix}, timeout=30)`;
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
return modified === content ? null : modified;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// P11. Replace os.system(cmd) with subprocess.run(shlex.split(cmd), check=True)
|
|
856
|
+
export function fixPythonOsSystem(content) {
|
|
857
|
+
const modified = content.replace(
|
|
858
|
+
/os\.system\((\w+)\)/g,
|
|
859
|
+
'subprocess.run(shlex.split($1), check=True)',
|
|
860
|
+
);
|
|
861
|
+
return modified === content ? null : modified;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// P12. Replace random.randint with secrets.randbelow in security contexts
|
|
865
|
+
export function fixPythonInsecureRandom(content) {
|
|
866
|
+
const lines = content.split('\n');
|
|
867
|
+
let changed = false;
|
|
868
|
+
const securityKeywords = /token|secret|key|password|auth|session|csrf|nonce|salt|otp|pin|code/i;
|
|
869
|
+
|
|
870
|
+
const result = lines.map((line) => {
|
|
871
|
+
if (/random\.randint\(/.test(line) && securityKeywords.test(line)) {
|
|
872
|
+
changed = true;
|
|
873
|
+
return line.replace(
|
|
874
|
+
/random\.randint\(\s*\d+\s*,\s*(\w+|\d+)\s*\)/g,
|
|
875
|
+
'secrets.randbelow($1)',
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return line;
|
|
879
|
+
}).join('\n');
|
|
880
|
+
|
|
881
|
+
return changed ? result : null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// P13. Replace cursor.execute("SELECT... %s" % var) with parameterized query
|
|
885
|
+
export function fixPythonSqlFormat(content) {
|
|
886
|
+
const modified = content.replace(
|
|
887
|
+
/cursor\.execute\(\s*"([^"]*%s[^"]*)"\s*%\s*(\w+)\s*\)/g,
|
|
888
|
+
'cursor.execute("$1", ($2,))',
|
|
889
|
+
);
|
|
890
|
+
return modified === content ? null : modified;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// P14. Replace app.run(debug=True) with env-based debug
|
|
894
|
+
export function fixPythonFlaskDebug(content) {
|
|
895
|
+
const modified = content.replace(
|
|
896
|
+
/app\.run\(([^)]*)debug\s*=\s*True([^)]*)\)/g,
|
|
897
|
+
`app.run($1debug=os.environ.get('FLASK_DEBUG', False)$2)`,
|
|
898
|
+
);
|
|
899
|
+
return modified === content ? null : modified;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// P15. Replace tempfile.mktemp() with tempfile.mkstemp()
|
|
903
|
+
export function fixPythonMktemp(content) {
|
|
904
|
+
const modified = content.replace(
|
|
905
|
+
/tempfile\.mktemp\(\)/g,
|
|
906
|
+
'tempfile.mkstemp()',
|
|
907
|
+
);
|
|
908
|
+
return modified === content ? null : modified;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// P16. Add Py2/Py3 safety comment for input()
|
|
912
|
+
export function fixPythonInputPy2(content) {
|
|
913
|
+
const modified = content.replace(
|
|
914
|
+
/^(\s*)(\w+\s*=\s*)input\(([^)]*)\)/gm,
|
|
915
|
+
'$1$2input($3) # NOTE: In Python 2, use raw_input() instead of input() for safety',
|
|
916
|
+
);
|
|
917
|
+
return modified === content ? null : modified;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// P17. Replace assert for security checks with proper if/raise
|
|
921
|
+
export function fixPythonAssertSecurity(content) {
|
|
922
|
+
const modified = content.replace(
|
|
923
|
+
/^(\s*)assert\s+(user\.\w+|is_admin|is_authenticated|has_permission\([^)]*\))/gm,
|
|
924
|
+
'$1if not $2:\n$1 raise PermissionError("Access denied")',
|
|
925
|
+
);
|
|
926
|
+
return modified === content ? null : modified;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// P18. Replace 0.0.0.0 with 127.0.0.1 in bind calls
|
|
930
|
+
export function fixPythonBindAllInterfaces(content) {
|
|
931
|
+
const modified = content.replace(
|
|
932
|
+
/(\.(?:bind|run|listen)\([^)]*)(["'])0\.0\.0\.0\2/g,
|
|
933
|
+
'$1$2127.0.0.1$2',
|
|
934
|
+
);
|
|
935
|
+
return modified === content ? null : modified;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// P19. Replace etree.parse( with defusedxml.parse(
|
|
939
|
+
export function fixPythonXmlParse(content) {
|
|
940
|
+
const modified = content.replace(
|
|
941
|
+
/etree\.parse\(/g,
|
|
942
|
+
'defusedxml.parse(',
|
|
943
|
+
);
|
|
944
|
+
return modified === content ? null : modified;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// P20. Replace render_template_string(user_data) with safe version
|
|
948
|
+
export function fixPythonRenderTemplateString(content) {
|
|
949
|
+
const modified = content.replace(
|
|
950
|
+
/render_template_string\((\w+)\)/g,
|
|
951
|
+
'render_template_string(safe_template, data=$1)',
|
|
952
|
+
);
|
|
953
|
+
return modified === content ? null : modified;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ---------------------------------------------------------------------------
|
|
957
|
+
// Go auto-fix functions
|
|
958
|
+
// ---------------------------------------------------------------------------
|
|
959
|
+
|
|
960
|
+
// G1. Replace db.Query(fmt.Sprintf("...%s", var)) with parameterized query
|
|
961
|
+
export function fixGoFmtSprintfSql(content) {
|
|
962
|
+
const modified = content.replace(
|
|
963
|
+
/db\.Query\(\s*fmt\.Sprintf\(\s*"([^"]*?)%s([^"]*?)"\s*,\s*(\w+)\s*\)\s*\)/g,
|
|
964
|
+
(match, before, after, variable) => {
|
|
965
|
+
return `db.Query("${before}$1${after}", ${variable})`;
|
|
966
|
+
},
|
|
967
|
+
);
|
|
968
|
+
return modified === content ? null : modified;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// G2. Replace InsecureSkipVerify: true with false
|
|
972
|
+
export function fixGoInsecureSkipVerify(content) {
|
|
973
|
+
const modified = content.replace(
|
|
974
|
+
/InsecureSkipVerify:\s*true/g,
|
|
975
|
+
'InsecureSkipVerify: false',
|
|
976
|
+
);
|
|
977
|
+
return modified === content ? null : modified;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// G3. Replace crypto/md5 import with crypto/sha256
|
|
981
|
+
export function fixGoMd5Import(content) {
|
|
982
|
+
const modified = content.replace(
|
|
983
|
+
/"crypto\/md5"/g,
|
|
984
|
+
'"crypto/sha256"',
|
|
985
|
+
);
|
|
986
|
+
return modified === content ? null : modified;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// G4. Replace result, _ := someFunc() with result, err := someFunc() + error check
|
|
990
|
+
export function fixGoIgnoredError(content) {
|
|
991
|
+
const modified = content.replace(
|
|
992
|
+
/^(\s*)(\w+),\s*_\s*:=\s*(.+)$/gm,
|
|
993
|
+
(match, indent, result, call) => {
|
|
994
|
+
return `${indent}${result}, err := ${call}\n${indent}if err != nil {\n${indent}\treturn err\n${indent}}`;
|
|
995
|
+
},
|
|
996
|
+
);
|
|
997
|
+
return modified === content ? null : modified;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// G5. Add Timeout to &http.Client{}
|
|
1001
|
+
export function fixGoHttpNoTimeout(content) {
|
|
1002
|
+
const modified = content.replace(
|
|
1003
|
+
/&http\.Client\{\s*\}/g,
|
|
1004
|
+
'&http.Client{Timeout: 30 * time.Second}',
|
|
1005
|
+
);
|
|
1006
|
+
return modified === content ? null : modified;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// G6. Replace :8080 with 127.0.0.1:8080 in net.Listen
|
|
1010
|
+
export function fixGoBindAllInterfaces(content) {
|
|
1011
|
+
const modified = content.replace(
|
|
1012
|
+
/(net\.Listen\(\s*"[^"]*"\s*,\s*"):(\d+"\s*\))/g,
|
|
1013
|
+
'$1127.0.0.1:$2',
|
|
1014
|
+
);
|
|
1015
|
+
return modified === content ? null : modified;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// G7. Replace template.HTML(userInput) with template.HTMLEscapeString(userInput)
|
|
1019
|
+
export function fixGoTemplateHtml(content) {
|
|
1020
|
+
const modified = content.replace(
|
|
1021
|
+
/template\.HTML\((\w+)\)/g,
|
|
1022
|
+
'template.HTMLEscapeString($1)',
|
|
1023
|
+
);
|
|
1024
|
+
return modified === content ? null : modified;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// G8. Add comment warning when exec.Command uses variable
|
|
1028
|
+
export function fixGoExecCommand(content) {
|
|
1029
|
+
const lines = content.split('\n');
|
|
1030
|
+
let changed = false;
|
|
1031
|
+
const result = lines.map((line) => {
|
|
1032
|
+
if (/exec\.Command\(/.test(line) && !/exec\.Command\(\s*"[^"]*"\s*\)/.test(line) && !/SECURITY/.test(line)) {
|
|
1033
|
+
changed = true;
|
|
1034
|
+
return `/* SECURITY: exec.Command with variable input — validate/sanitize before use */ ${line}`;
|
|
1035
|
+
}
|
|
1036
|
+
return line;
|
|
1037
|
+
}).join('\n');
|
|
1038
|
+
return changed ? result : null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// G9. Replace math/rand with crypto/rand in security contexts
|
|
1042
|
+
export function fixGoWeakRand(content) {
|
|
1043
|
+
const modified = content.replace(
|
|
1044
|
+
/"math\/rand"/g,
|
|
1045
|
+
'"crypto/rand"',
|
|
1046
|
+
);
|
|
1047
|
+
return modified === content ? null : modified;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// G10. Replace defer f.Close() with defer func() { _ = f.Close() }()
|
|
1051
|
+
export function fixGoDeferClose(content) {
|
|
1052
|
+
const modified = content.replace(
|
|
1053
|
+
/defer\s+(\w+)\.Close\(\)/g,
|
|
1054
|
+
'defer func() { _ = $1.Close() }()',
|
|
1055
|
+
);
|
|
1056
|
+
return modified === content ? null : modified;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ---------------------------------------------------------------------------
|
|
1060
|
+
// Ruby auto-fix functions
|
|
1061
|
+
// ---------------------------------------------------------------------------
|
|
1062
|
+
|
|
1063
|
+
// R1. Replace find_by_sql("...#{var}") with parameterized version
|
|
1064
|
+
export function fixRubyFindBySql(content) {
|
|
1065
|
+
const modified = content.replace(
|
|
1066
|
+
/find_by_sql\(\s*"([^"]*?)#\{(\w+)\}([^"]*?)"\s*\)/g,
|
|
1067
|
+
(match, before, variable, after) => {
|
|
1068
|
+
return `find_by_sql(["${before}?${after}", ${variable}])`;
|
|
1069
|
+
},
|
|
1070
|
+
);
|
|
1071
|
+
return modified === content ? null : modified;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// R2. Replace .html_safe with sanitize() wrapper
|
|
1075
|
+
export function fixRubyHtmlSafe(content) {
|
|
1076
|
+
const modified = content.replace(
|
|
1077
|
+
/(\w+(?:\.\w+)*)\.html_safe/g,
|
|
1078
|
+
'sanitize($1)',
|
|
1079
|
+
);
|
|
1080
|
+
return modified === content ? null : modified;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// R3. Replace system("cmd #{var}") with system("cmd", var)
|
|
1084
|
+
export function fixRubySystemCall(content) {
|
|
1085
|
+
const modified = content.replace(
|
|
1086
|
+
/system\(\s*"([^"]*?)#\{(\w+)\}([^"]*?)"\s*\)/g,
|
|
1087
|
+
(match, before, variable, after) => {
|
|
1088
|
+
const cmd = (before + after).trim();
|
|
1089
|
+
return `system("${cmd}", ${variable})`;
|
|
1090
|
+
},
|
|
1091
|
+
);
|
|
1092
|
+
return modified === content ? null : modified;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// R4. Replace YAML.load( with YAML.safe_load(
|
|
1096
|
+
export function fixRubyYamlLoad(content) {
|
|
1097
|
+
const modified = content.replace(
|
|
1098
|
+
/YAML\.load\(/g,
|
|
1099
|
+
'YAML.safe_load(',
|
|
1100
|
+
);
|
|
1101
|
+
return modified === content ? null : modified;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// R5. Add warning comment to Marshal.load usage
|
|
1105
|
+
export function fixRubyMarshalLoad(content) {
|
|
1106
|
+
const lines = content.split('\n');
|
|
1107
|
+
let changed = false;
|
|
1108
|
+
const result = lines.map((line) => {
|
|
1109
|
+
if (/Marshal\.load\(/.test(line) && !/SECURITY/.test(line)) {
|
|
1110
|
+
changed = true;
|
|
1111
|
+
return `# SECURITY WARNING: Marshal.load can execute arbitrary code — use JSON.parse instead\n${line}`;
|
|
1112
|
+
}
|
|
1113
|
+
return line;
|
|
1114
|
+
}).join('\n');
|
|
1115
|
+
return changed ? result : null;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// R6. Replace permit! with permit(:specific_fields) + TODO comment
|
|
1119
|
+
export function fixRubyPermitAll(content) {
|
|
1120
|
+
const modified = content.replace(
|
|
1121
|
+
/\.permit!/g,
|
|
1122
|
+
'.permit(:id) # TODO: replace :id with actual permitted fields',
|
|
1123
|
+
);
|
|
1124
|
+
return modified === content ? null : modified;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// R7. Comment out skip_forgery_protection with warning
|
|
1128
|
+
export function fixRubySkipCsrf(content) {
|
|
1129
|
+
const modified = content.replace(
|
|
1130
|
+
/^(\s*)(skip_forgery_protection.*)/gm,
|
|
1131
|
+
'$1# SECURITY WARNING: CSRF protection disabled — re-enable in production\n$1# $2',
|
|
1132
|
+
);
|
|
1133
|
+
return modified === content ? null : modified;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// R8. Replace eval(user_input) with safer alternative + comment
|
|
1137
|
+
export function fixRubyEval(content) {
|
|
1138
|
+
const modified = content.replace(
|
|
1139
|
+
/\beval\((\w+)\)/g,
|
|
1140
|
+
'# SECURITY: eval is unsafe — use a sandboxed evaluator or whitelist approach\nJSON.parse($1) # TODO: replace with appropriate safe alternative',
|
|
1141
|
+
);
|
|
1142
|
+
return modified === content ? null : modified;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// R9. Replace open(url) with URI.open(url)
|
|
1146
|
+
export function fixRubyOpenUri(content) {
|
|
1147
|
+
const modified = content.replace(
|
|
1148
|
+
/(?<!\w)(?<!\.)open\((\s*(?:url|uri|link|\w+_url|\w+_uri)\s*)\)/gi,
|
|
1149
|
+
'URI.open($1)',
|
|
1150
|
+
);
|
|
1151
|
+
return modified === content ? null : modified;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// R10. Replace Digest::MD5 with Digest::SHA256
|
|
1155
|
+
export function fixRubyWeakHash(content) {
|
|
1156
|
+
const modified = content.replace(
|
|
1157
|
+
/Digest::MD5/g,
|
|
1158
|
+
'Digest::SHA256',
|
|
1159
|
+
);
|
|
1160
|
+
return modified === content ? null : modified;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// ---------------------------------------------------------------------------
|
|
1164
|
+
// PHP auto-fix functions
|
|
1165
|
+
// ---------------------------------------------------------------------------
|
|
1166
|
+
|
|
1167
|
+
// P1. Replace mysql_query("...{$var}") with PDO prepared statement
|
|
1168
|
+
export function fixPhpMysqlQuery(content) {
|
|
1169
|
+
const modified = content.replace(
|
|
1170
|
+
/mysql_query\(\s*"([^"]*?)\{\$(\w+)\}([^"]*?)"\s*\)/g,
|
|
1171
|
+
(match, before, variable, after) => {
|
|
1172
|
+
return `$stmt = $pdo->prepare("${before}?${after}"); $stmt->execute([$${variable}])`;
|
|
1173
|
+
},
|
|
1174
|
+
);
|
|
1175
|
+
return modified === content ? null : modified;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// P2. Replace echo $_GET['x'] with echo htmlspecialchars($_GET['x'])
|
|
1179
|
+
export function fixPhpEcho(content) {
|
|
1180
|
+
const modified = content.replace(
|
|
1181
|
+
/echo\s+(\$_(?:GET|POST|REQUEST)\s*\[\s*['"][^'"]+['"]\s*\])/g,
|
|
1182
|
+
(match, variable) => {
|
|
1183
|
+
if (match.includes('htmlspecialchars')) return match;
|
|
1184
|
+
return `echo htmlspecialchars(${variable}, ENT_QUOTES, 'UTF-8')`;
|
|
1185
|
+
},
|
|
1186
|
+
);
|
|
1187
|
+
return modified === content ? null : modified;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// P3. Add security warning comment to eval() usage (PHP)
|
|
1191
|
+
export function fixPhpEval(content) {
|
|
1192
|
+
const lines = content.split('\n');
|
|
1193
|
+
let changed = false;
|
|
1194
|
+
const result = lines.map((line) => {
|
|
1195
|
+
if (/\beval\s*\(/.test(line) && !/SECURITY/.test(line)) {
|
|
1196
|
+
changed = true;
|
|
1197
|
+
return `/* SECURITY WARNING: eval() executes arbitrary code — remove or replace with safe alternative */ ${line}`;
|
|
1198
|
+
}
|
|
1199
|
+
return line;
|
|
1200
|
+
}).join('\n');
|
|
1201
|
+
return changed ? result : null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// P4. Replace exec($cmd) with escapeshellarg() wrapped version
|
|
1205
|
+
export function fixPhpExec(content) {
|
|
1206
|
+
const modified = content.replace(
|
|
1207
|
+
/\bexec\(\s*(\$\w+)\s*\)/g,
|
|
1208
|
+
'exec(escapeshellcmd(escapeshellarg($1)))',
|
|
1209
|
+
);
|
|
1210
|
+
return modified === content ? null : modified;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// P5. Replace md5($password) with password_hash($password, PASSWORD_DEFAULT)
|
|
1214
|
+
export function fixPhpMd5Password(content) {
|
|
1215
|
+
const modified = content.replace(
|
|
1216
|
+
/md5\(\s*(\$password|\$pass|\$passwd|\$pwd)\s*\)/g,
|
|
1217
|
+
'password_hash($1, PASSWORD_DEFAULT)',
|
|
1218
|
+
);
|
|
1219
|
+
return modified === content ? null : modified;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// P6. Replace extract($_POST) with explicit variable assignment
|
|
1223
|
+
export function fixPhpExtract(content) {
|
|
1224
|
+
const modified = content.replace(
|
|
1225
|
+
/extract\(\s*(\$_(POST|GET|REQUEST))\s*\)/g,
|
|
1226
|
+
(match, superglobal) => {
|
|
1227
|
+
return `/* SECURITY: extract() replaced — assign variables explicitly */\n// TODO: Replace with explicit assignments, e.g.: $name = ${superglobal}['name'];\n// extract(${superglobal})`;
|
|
1228
|
+
},
|
|
1229
|
+
);
|
|
1230
|
+
return modified === content ? null : modified;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// P7. Replace == with === in auth contexts (PHP)
|
|
1234
|
+
export function fixPhpLooseComparison(content) {
|
|
1235
|
+
const authKeywords = /password|token|secret|auth|session|user|role|admin|login|hash/i;
|
|
1236
|
+
const lines = content.split('\n');
|
|
1237
|
+
let changed = false;
|
|
1238
|
+
const result = lines.map((line) => {
|
|
1239
|
+
if (/^\s*\/\//.test(line) || /^\s*\/\*/.test(line) || /^\s*\*/.test(line) || /^\s*#/.test(line)) return line;
|
|
1240
|
+
if (authKeywords.test(line) && /[^!=<>]==[^=]/.test(line)) {
|
|
1241
|
+
changed = true;
|
|
1242
|
+
return line.replace(/([^!=<>])={2}([^=])/g, '$1===$2');
|
|
1243
|
+
}
|
|
1244
|
+
return line;
|
|
1245
|
+
}).join('\n');
|
|
1246
|
+
return changed ? result : null;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// P8. Add session.cookie_httponly and session.cookie_secure
|
|
1250
|
+
export function fixPhpSessionConfig(content) {
|
|
1251
|
+
if (!/session_start\s*\(/.test(content)) return null;
|
|
1252
|
+
if (/cookie_httponly/.test(content) && /cookie_secure/.test(content)) return null;
|
|
1253
|
+
const modified = content.replace(
|
|
1254
|
+
/session_start\s*\(\s*\)/g,
|
|
1255
|
+
"ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 1); session_start()",
|
|
1256
|
+
);
|
|
1257
|
+
return modified === content ? null : modified;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// P9. Add ['allowed_classes' => false] to unserialize()
|
|
1261
|
+
export function fixPhpUnserialize(content) {
|
|
1262
|
+
const modified = content.replace(
|
|
1263
|
+
/unserialize\(\s*(\$\w+)\s*\)/g,
|
|
1264
|
+
"unserialize($1, ['allowed_classes' => false])",
|
|
1265
|
+
);
|
|
1266
|
+
return modified === content ? null : modified;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// P10. Add warning to include($_GET[...]) with whitelist suggestion
|
|
1270
|
+
export function fixPhpInclude(content) {
|
|
1271
|
+
const modified = content.replace(
|
|
1272
|
+
/^(\s*)((?:include|require)(?:_once)?\s*\(\s*\$_(?:GET|POST|REQUEST)\s*\[.*?\]\s*\))/gm,
|
|
1273
|
+
'$1/* SECURITY: Never include files from user input — use a whitelist of allowed paths */\n$1// $2',
|
|
1274
|
+
);
|
|
1275
|
+
return modified === content ? null : modified;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Registry of all pattern-based fix functions, keyed by fix ID.
|
|
1280
|
+
*/
|
|
1281
|
+
export const patternFixes = {
|
|
1282
|
+
'fix-eval': { fn: fixEval, tier: 2, title: 'Replace eval() with safer alternative' },
|
|
1283
|
+
'fix-math-random': { fn: fixMathRandom, tier: 1, title: 'Replace Math.random() with crypto.randomUUID()' },
|
|
1284
|
+
'fix-cookie-flags': { fn: fixCookieFlags, tier: 1, title: 'Add httpOnly/secure/sameSite to cookies' },
|
|
1285
|
+
'fix-http-urls': { fn: fixHttpUrls, tier: 1, title: 'Replace http:// with https://' },
|
|
1286
|
+
'fix-helmet-missing': { fn: fixHelmetMissing, tier: 1, title: 'Add helmet() middleware' },
|
|
1287
|
+
'fix-loose-equality': { fn: fixLooseEquality, tier: 1, title: 'Replace == with === in auth comparisons' },
|
|
1288
|
+
'fix-jwt-verify-alg': { fn: fixJwtVerifyAlgorithm, tier: 1, title: 'Add algorithms option to jwt.verify()' },
|
|
1289
|
+
'fix-weak-hash': { fn: fixWeakHash, tier: 1, title: 'Replace MD5/SHA1 with SHA256' },
|
|
1290
|
+
'fix-sql-terminator': { fn: fixSqlTerminator, tier: 2, title: 'Add SQL comment terminator' },
|
|
1291
|
+
'fix-exec-to-execfile': { fn: fixExecToExecFile, tier: 2, title: 'Replace exec() with execFile()' },
|
|
1292
|
+
'fix-jwt-sign-expiry': { fn: fixJwtSignExpiry, tier: 1, title: 'Add expiresIn to jwt.sign()' },
|
|
1293
|
+
'fix-rate-limit': { fn: fixRateLimitMissing, tier: 2, title: 'Add rate-limit middleware' },
|
|
1294
|
+
'fix-json-parse-body': { fn: fixJsonParseReqBody, tier: 2, title: 'Replace JSON.parse(req.body) with body-parser' },
|
|
1295
|
+
'fix-sql-escape': { fn: fixSqlEscape, tier: 2, title: 'Add escape() to SQL user input' },
|
|
1296
|
+
'fix-csp-header': { fn: fixContentSecurityPolicy, tier: 1, title: 'Add Content-Security-Policy via helmet' },
|
|
1297
|
+
'fix-console-log': { fn: fixConsoleLog, tier: 1, title: 'Replace console.log with logger' },
|
|
1298
|
+
'fix-ts-return-type': { fn: fixTsReturnType, tier: 1, title: 'Add return type to exported TS functions' },
|
|
1299
|
+
'fix-var-to-const': { fn: fixVarToConstLet, tier: 1, title: 'Convert var to const/let' },
|
|
1300
|
+
'fix-empty-catch': { fn: fixEmptyCatch, tier: 1, title: 'Add error parameter to empty catch blocks' },
|
|
1301
|
+
'fix-callback-to-async': { fn: fixCallbackToAsync, tier: 2, title: 'Convert callback-style to async/await' },
|
|
1302
|
+
'fix-python-debug-true': { fn: fixPythonDebugTrue, tier: 1, title: 'Replace Python DEBUG = True with env-based config' },
|
|
1303
|
+
'fix-python-hardcoded-secret': { fn: fixPythonHardcodedSecret, tier: 1, title: 'Replace hardcoded SECRET_KEY with os.environ' },
|
|
1304
|
+
'fix-python-shell-true': { fn: fixPythonShellTrue, tier: 2, title: 'Replace subprocess shell=True with shlex.split' },
|
|
1305
|
+
'fix-python-eval': { fn: fixPythonEval, tier: 2, title: 'Replace Python eval() with ast.literal_eval()' },
|
|
1306
|
+
'fix-python-pickle-load': { fn: fixPythonPickleLoad, tier: 2, title: 'Replace pickle.loads() with json.loads()' },
|
|
1307
|
+
'fix-python-yaml-load': { fn: fixPythonYamlLoad, tier: 1, title: 'Replace yaml.load() with yaml.safe_load()' },
|
|
1308
|
+
'fix-python-md5': { fn: fixPythonMd5, tier: 1, title: 'Replace hashlib.md5 with hashlib.sha256' },
|
|
1309
|
+
'fix-python-sha1': { fn: fixPythonSha1, tier: 1, title: 'Replace hashlib.sha1 with hashlib.sha256' },
|
|
1310
|
+
'fix-python-requests-no-verify': { fn: fixPythonRequestsNoVerify, tier: 1, title: 'Replace verify=False with verify=True in requests' },
|
|
1311
|
+
'fix-python-requests-no-timeout': { fn: fixPythonRequestsNoTimeout, tier: 1, title: 'Add timeout to requests calls' },
|
|
1312
|
+
'fix-python-os-system': { fn: fixPythonOsSystem, tier: 2, title: 'Replace os.system() with subprocess.run()' },
|
|
1313
|
+
'fix-python-insecure-random': { fn: fixPythonInsecureRandom, tier: 2, title: 'Replace random.randint with secrets.randbelow' },
|
|
1314
|
+
'fix-python-sql-format': { fn: fixPythonSqlFormat, tier: 2, title: 'Replace string-formatted SQL with parameterized query' },
|
|
1315
|
+
'fix-python-flask-debug': { fn: fixPythonFlaskDebug, tier: 1, title: 'Replace Flask debug=True with env-based config' },
|
|
1316
|
+
'fix-python-mktemp': { fn: fixPythonMktemp, tier: 1, title: 'Replace tempfile.mktemp() with tempfile.mkstemp()' },
|
|
1317
|
+
'fix-python-input-py2': { fn: fixPythonInputPy2, tier: 1, title: 'Add Python 2 safety comment for input()' },
|
|
1318
|
+
'fix-python-assert-security': { fn: fixPythonAssertSecurity, tier: 2, title: 'Replace assert with proper permission check' },
|
|
1319
|
+
'fix-python-bind-all-interfaces': { fn: fixPythonBindAllInterfaces, tier: 1, title: 'Replace 0.0.0.0 with 127.0.0.1 in bind calls' },
|
|
1320
|
+
'fix-python-xml-parse': { fn: fixPythonXmlParse, tier: 2, title: 'Replace etree.parse with defusedxml.parse' },
|
|
1321
|
+
'fix-python-render-template-string': { fn: fixPythonRenderTemplateString, tier: 2, title: 'Replace render_template_string with safe version' },
|
|
1322
|
+
// Go fixes
|
|
1323
|
+
'fix-go-fmt-sprintf-sql': { fn: fixGoFmtSprintfSql, tier: 2, title: 'Replace fmt.Sprintf SQL with parameterized query' },
|
|
1324
|
+
'fix-go-insecure-skip-verify': { fn: fixGoInsecureSkipVerify, tier: 1, title: 'Disable InsecureSkipVerify in TLS config' },
|
|
1325
|
+
'fix-go-md5-import': { fn: fixGoMd5Import, tier: 1, title: 'Replace crypto/md5 with crypto/sha256' },
|
|
1326
|
+
'fix-go-ignored-error': { fn: fixGoIgnoredError, tier: 2, title: 'Handle ignored Go error return values' },
|
|
1327
|
+
'fix-go-http-no-timeout': { fn: fixGoHttpNoTimeout, tier: 1, title: 'Add timeout to http.Client' },
|
|
1328
|
+
'fix-go-bind-all-interfaces': { fn: fixGoBindAllInterfaces, tier: 1, title: 'Replace :port with 127.0.0.1:port in net.Listen' },
|
|
1329
|
+
'fix-go-template-html': { fn: fixGoTemplateHtml, tier: 2, title: 'Replace template.HTML with HTMLEscapeString' },
|
|
1330
|
+
'fix-go-exec-command': { fn: fixGoExecCommand, tier: 2, title: 'Add warning to exec.Command with variable input' },
|
|
1331
|
+
'fix-go-weak-rand': { fn: fixGoWeakRand, tier: 1, title: 'Replace math/rand with crypto/rand' },
|
|
1332
|
+
'fix-go-defer-close': { fn: fixGoDeferClose, tier: 1, title: 'Wrap defer Close() in anonymous function' },
|
|
1333
|
+
// Ruby fixes
|
|
1334
|
+
'fix-ruby-find-by-sql': { fn: fixRubyFindBySql, tier: 2, title: 'Replace find_by_sql interpolation with parameterized query' },
|
|
1335
|
+
'fix-ruby-html-safe': { fn: fixRubyHtmlSafe, tier: 2, title: 'Replace html_safe with sanitize()' },
|
|
1336
|
+
'fix-ruby-system-call': { fn: fixRubySystemCall, tier: 2, title: 'Replace system() interpolation with argument list' },
|
|
1337
|
+
'fix-ruby-yaml-load': { fn: fixRubyYamlLoad, tier: 1, title: 'Replace YAML.load with YAML.safe_load' },
|
|
1338
|
+
'fix-ruby-marshal-load': { fn: fixRubyMarshalLoad, tier: 2, title: 'Add warning to Marshal.load usage' },
|
|
1339
|
+
'fix-ruby-permit-all': { fn: fixRubyPermitAll, tier: 2, title: 'Replace permit! with explicit field list' },
|
|
1340
|
+
'fix-ruby-skip-csrf': { fn: fixRubySkipCsrf, tier: 2, title: 'Comment out skip_forgery_protection' },
|
|
1341
|
+
'fix-ruby-eval': { fn: fixRubyEval, tier: 2, title: 'Replace Ruby eval() with safer alternative' },
|
|
1342
|
+
'fix-ruby-open-uri': { fn: fixRubyOpenUri, tier: 1, title: 'Replace open(url) with URI.open(url)' },
|
|
1343
|
+
'fix-ruby-weak-hash': { fn: fixRubyWeakHash, tier: 1, title: 'Replace Digest::MD5 with Digest::SHA256' },
|
|
1344
|
+
// PHP fixes
|
|
1345
|
+
'fix-php-mysql-query': { fn: fixPhpMysqlQuery, tier: 2, title: 'Replace mysql_query with PDO prepared statement' },
|
|
1346
|
+
'fix-php-echo': { fn: fixPhpEcho, tier: 1, title: 'Add htmlspecialchars to echoed user input' },
|
|
1347
|
+
'fix-php-eval': { fn: fixPhpEval, tier: 2, title: 'Add security warning to PHP eval()' },
|
|
1348
|
+
'fix-php-exec': { fn: fixPhpExec, tier: 2, title: 'Add escapeshellarg to exec() calls' },
|
|
1349
|
+
'fix-php-md5-password': { fn: fixPhpMd5Password, tier: 1, title: 'Replace md5() with password_hash()' },
|
|
1350
|
+
'fix-php-extract': { fn: fixPhpExtract, tier: 2, title: 'Replace extract() with explicit assignments' },
|
|
1351
|
+
'fix-php-loose-comparison': { fn: fixPhpLooseComparison, tier: 1, title: 'Replace == with === in PHP auth contexts' },
|
|
1352
|
+
'fix-php-session-config': { fn: fixPhpSessionConfig, tier: 1, title: 'Add secure session cookie settings' },
|
|
1353
|
+
'fix-php-unserialize': { fn: fixPhpUnserialize, tier: 2, title: 'Add allowed_classes restriction to unserialize()' },
|
|
1354
|
+
'fix-php-include': { fn: fixPhpInclude, tier: 2, title: 'Add warning to include with user input' },
|
|
1355
|
+
// Advanced JS/TS fixes (21-40)
|
|
1356
|
+
'fix-dangerously-set-innerhtml': { fn: fixDangerouslySetInnerHTML, tier: 2, title: 'Wrap dangerouslySetInnerHTML with DOMPurify.sanitize' },
|
|
1357
|
+
'fix-document-write': { fn: fixDocumentWrite, tier: 2, title: 'Replace document.write with textContent' },
|
|
1358
|
+
'fix-innerHTML': { fn: fixInnerHTML, tier: 2, title: 'Replace innerHTML with textContent' },
|
|
1359
|
+
'fix-nosql-injection': { fn: fixNoSqlInjection, tier: 2, title: 'Replace unsafe $where with safe MongoDB query' },
|
|
1360
|
+
'fix-prototype-pollution': { fn: fixPrototypePollution, tier: 2, title: 'Add null prototype to Object.assign' },
|
|
1361
|
+
'fix-timing-attack': { fn: fixTimingAttack, tier: 2, title: 'Replace === with crypto.timingSafeEqual for secrets' },
|
|
1362
|
+
'fix-path-traversal': { fn: fixPathTraversal, tier: 2, title: 'Add path traversal guard to fs operations' },
|
|
1363
|
+
'fix-regex-dos': { fn: fixRegexDos, tier: 2, title: 'Sanitize user input in RegExp constructor' },
|
|
1364
|
+
'fix-open-redirect': { fn: fixOpenRedirect, tier: 2, title: 'Add URL validation before redirect' },
|
|
1365
|
+
'fix-no-rate-limit': { fn: fixNoRateLimit, tier: 2, title: 'Add express-rate-limit to route handlers' },
|
|
1366
|
+
'fix-xss-query-param': { fn: fixXssQueryParam, tier: 2, title: 'Escape req.query in response output' },
|
|
1367
|
+
'fix-hardcoded-ip': { fn: fixHardcodedIp, tier: 1, title: 'Replace hardcoded 0.0.0.0 with env-based host' },
|
|
1368
|
+
'fix-no-helmet-csp': { fn: fixNoHelmetCsp, tier: 1, title: 'Add CSP directives to helmet()' },
|
|
1369
|
+
'fix-sql-concat': { fn: fixSqlConcat, tier: 2, title: 'Replace SQL string concat with parameterized query' },
|
|
1370
|
+
'fix-jwt-storage-local': { fn: fixJwtStorageLocal, tier: 2, title: 'Replace localStorage JWT with httpOnly cookie' },
|
|
1371
|
+
'fix-cors-wildcard': { fn: fixCorsWildcard, tier: 1, title: 'Replace CORS origin wildcard with specific origins' },
|
|
1372
|
+
'fix-no-input-validation': { fn: fixNoInputValidation, tier: 2, title: 'Add input validation stub for req.body' },
|
|
1373
|
+
'fix-console-error': { fn: fixConsoleError, tier: 1, title: 'Replace console.error with logger.error' },
|
|
1374
|
+
'fix-await-in-loop': { fn: fixAwaitInLoop, tier: 2, title: 'Replace await-in-loop with Promise.all' },
|
|
1375
|
+
'fix-missing-error-handler': { fn: fixMissingErrorHandler, tier: 2, title: 'Add Express error handler middleware' },
|
|
1376
|
+
// --- Data-driven registry fixes (340 entries) ---
|
|
1377
|
+
...registryPatternFixes,
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Apply a pattern-based fix by ID. Returns { content, applied } or null.
|
|
1382
|
+
*
|
|
1383
|
+
* @param {string} fixId - The fix identifier from patternFixes registry.
|
|
1384
|
+
* @param {string} content - The file content to transform.
|
|
1385
|
+
* @param {object} [finding] - Optional finding context.
|
|
1386
|
+
* @returns {{ content: string, applied: boolean } | null}
|
|
1387
|
+
*/
|
|
1388
|
+
export function applyPatternFix(fixId, content, finding = {}) {
|
|
1389
|
+
const entry = patternFixes[fixId];
|
|
1390
|
+
if (!entry) return null;
|
|
1391
|
+
const result = entry.fn(content, finding);
|
|
1392
|
+
if (result === null) return { content, applied: false };
|
|
1393
|
+
return { content: result, applied: true };
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Auto-detect which pattern fixes apply to the given content and apply all of them.
|
|
1398
|
+
* Returns { content, appliedFixes: string[] }.
|
|
1399
|
+
*/
|
|
1400
|
+
export function applyAllPatternFixes(content, finding = {}) {
|
|
1401
|
+
let current = content;
|
|
1402
|
+
const appliedFixes = [];
|
|
1403
|
+
|
|
1404
|
+
for (const [id, entry] of Object.entries(patternFixes)) {
|
|
1405
|
+
const result = entry.fn(current, finding);
|
|
1406
|
+
if (result !== null) {
|
|
1407
|
+
current = result;
|
|
1408
|
+
appliedFixes.push(id);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return { content: current, appliedFixes };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Generate a unified diff string between old and new content (for --dry-run).
|
|
1417
|
+
*/
|
|
1418
|
+
export function generateDiff(oldContent, newContent, filename = 'file') {
|
|
1419
|
+
const oldLines = oldContent.split('\n');
|
|
1420
|
+
const newLines = newContent.split('\n');
|
|
1421
|
+
const diff = [];
|
|
1422
|
+
|
|
1423
|
+
diff.push(`--- a/${filename}`);
|
|
1424
|
+
diff.push(`+++ b/${filename}`);
|
|
1425
|
+
|
|
1426
|
+
// Simple line-by-line diff
|
|
1427
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
1428
|
+
let inHunk = false;
|
|
1429
|
+
let hunkStart = -1;
|
|
1430
|
+
|
|
1431
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1432
|
+
const oldLine = i < oldLines.length ? oldLines[i] : undefined;
|
|
1433
|
+
const newLine = i < newLines.length ? newLines[i] : undefined;
|
|
1434
|
+
|
|
1435
|
+
if (oldLine !== newLine) {
|
|
1436
|
+
if (!inHunk) {
|
|
1437
|
+
hunkStart = Math.max(0, i - 2);
|
|
1438
|
+
diff.push(`@@ -${hunkStart + 1},${oldLines.length - hunkStart} +${hunkStart + 1},${newLines.length - hunkStart} @@`);
|
|
1439
|
+
// Context lines before
|
|
1440
|
+
for (let j = hunkStart; j < i; j++) {
|
|
1441
|
+
if (j < oldLines.length) diff.push(` ${oldLines[j]}`);
|
|
1442
|
+
}
|
|
1443
|
+
inHunk = true;
|
|
1444
|
+
}
|
|
1445
|
+
if (oldLine !== undefined) diff.push(`-${oldLine}`);
|
|
1446
|
+
if (newLine !== undefined) diff.push(`+${newLine}`);
|
|
1447
|
+
} else {
|
|
1448
|
+
if (inHunk) {
|
|
1449
|
+
// One context line after
|
|
1450
|
+
if (oldLine !== undefined) diff.push(` ${oldLine}`);
|
|
1451
|
+
inHunk = false;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return diff.join('\n');
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const TIER_LABELS = {
|
|
1460
|
+
1: chalk.green('safe'),
|
|
1461
|
+
2: chalk.yellow('review'),
|
|
1462
|
+
3: chalk.red('manual'),
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
const TIER_DESCRIPTIONS = {
|
|
1466
|
+
1: 'Auto-applied without confirmation',
|
|
1467
|
+
2: 'Requires review and confirmation',
|
|
1468
|
+
3: 'Manual changes only — instructions provided',
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Classify a finding into its fix tier.
|
|
1473
|
+
* Tier 1 (safe): Config changes, headers, .gitignore, creating security files.
|
|
1474
|
+
* Tier 2 (review): Code changes like parameterized queries, bcrypt swaps.
|
|
1475
|
+
* Tier 3 (manual): Architecture changes — never auto-applied.
|
|
1476
|
+
*/
|
|
1477
|
+
function getTier(finding) {
|
|
1478
|
+
return finding.fix?.tier ?? 2;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Generate a unified-diff-style preview for a fix.
|
|
1483
|
+
*/
|
|
1484
|
+
function formatDiff(fix, file) {
|
|
1485
|
+
const lines = [];
|
|
1486
|
+
if (fix.type === 'replace' && fix.old && fix.new) {
|
|
1487
|
+
lines.push(chalk.gray(` --- a/${file}`));
|
|
1488
|
+
lines.push(chalk.gray(` +++ b/${file}`));
|
|
1489
|
+
for (const l of fix.old.split('\n')) {
|
|
1490
|
+
lines.push(chalk.red(` - ${l}`));
|
|
1491
|
+
}
|
|
1492
|
+
for (const l of fix.new.split('\n')) {
|
|
1493
|
+
lines.push(chalk.green(` + ${l}`));
|
|
1494
|
+
}
|
|
1495
|
+
} else if (fix.type === 'insert' && fix.content) {
|
|
1496
|
+
lines.push(chalk.gray(` --- a/${file}`));
|
|
1497
|
+
lines.push(chalk.gray(` +++ b/${file}`));
|
|
1498
|
+
for (const l of fix.content.split('\n')) {
|
|
1499
|
+
lines.push(chalk.green(` + ${l}`));
|
|
1500
|
+
}
|
|
1501
|
+
} else if (fix.type === 'create') {
|
|
1502
|
+
lines.push(chalk.gray(` +++ b/${fix.filePath}`));
|
|
1503
|
+
for (const l of (fix.content || '').split('\n').slice(0, 10)) {
|
|
1504
|
+
lines.push(chalk.green(` + ${l}`));
|
|
1505
|
+
}
|
|
1506
|
+
const total = (fix.content || '').split('\n').length;
|
|
1507
|
+
if (total > 10) {
|
|
1508
|
+
lines.push(chalk.gray(` ... (${total - 10} more lines)`));
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return lines.join('\n');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Show a progress indicator: [3/10] style.
|
|
1516
|
+
*/
|
|
1517
|
+
function progress(current, total, label) {
|
|
1518
|
+
const bar = '='.repeat(Math.round((current / total) * 20)).padEnd(20, ' ');
|
|
1519
|
+
return ` [${bar}] ${current}/${total} ${label}`;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Check if a file is safe to modify (exists, under size limit).
|
|
1524
|
+
*/
|
|
1525
|
+
function isSafeToModify(fullPath) {
|
|
1526
|
+
try {
|
|
1527
|
+
const stat = statSync(fullPath);
|
|
1528
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
1529
|
+
return { safe: false, reason: `file exceeds ${MAX_FILE_SIZE / 1024}KB limit (${Math.round(stat.size / 1024)}KB)` };
|
|
1530
|
+
}
|
|
1531
|
+
return { safe: true };
|
|
1532
|
+
} catch {
|
|
1533
|
+
return { safe: false, reason: 'file not found' };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* After applying a fix, re-read the file and verify the triggering pattern is gone.
|
|
1539
|
+
*/
|
|
1540
|
+
function verifyFix(fullPath, fix) {
|
|
1541
|
+
if (fix.type === 'create') {
|
|
1542
|
+
return existsSync(join(fullPath, '..', fix.filePath)) || existsSync(fix.filePath);
|
|
1543
|
+
}
|
|
1544
|
+
if (fix.type === 'replace' && fix.old) {
|
|
1545
|
+
try {
|
|
1546
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
1547
|
+
return !content.includes(fix.old);
|
|
1548
|
+
} catch {
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return true; // inserts are assumed verified
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Count total lines changed across all applied fixes.
|
|
1557
|
+
*/
|
|
1558
|
+
function countLinesChanged(appliedFixes) {
|
|
1559
|
+
let added = 0;
|
|
1560
|
+
let removed = 0;
|
|
1561
|
+
for (const fix of appliedFixes) {
|
|
1562
|
+
if (fix.type === 'replace') {
|
|
1563
|
+
const oldText = fix.old || '';
|
|
1564
|
+
const newText = fix.new || '';
|
|
1565
|
+
removed += oldText === '' ? 0 : oldText.split('\n').length;
|
|
1566
|
+
added += newText === '' ? 0 : newText.split('\n').length;
|
|
1567
|
+
} else if (fix.type === 'insert') {
|
|
1568
|
+
const text = fix.content || '';
|
|
1569
|
+
added += text === '' ? 0 : text.split('\n').length;
|
|
1570
|
+
} else if (fix.type === 'create') {
|
|
1571
|
+
const text = fix.content || '';
|
|
1572
|
+
added += text === '' ? 0 : text.split('\n').length;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return { added, removed };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Create a git stash backup before applying fixes.
|
|
1580
|
+
* Returns true if stash was created.
|
|
1581
|
+
*/
|
|
1582
|
+
function createBackup(targetPath) {
|
|
1583
|
+
try {
|
|
1584
|
+
const before = execSync('git stash list', { cwd: targetPath, stdio: 'pipe' }).toString();
|
|
1585
|
+
execSync('git stash push -m "doorman-backup-before-fix"', {
|
|
1586
|
+
cwd: targetPath,
|
|
1587
|
+
stdio: 'pipe',
|
|
1588
|
+
});
|
|
1589
|
+
const after = execSync('git stash list', { cwd: targetPath, stdio: 'pipe' }).toString();
|
|
1590
|
+
// If stash list didn't change, there was nothing to stash
|
|
1591
|
+
return before !== after;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Pop the most recent doorman backup from git stash.
|
|
1599
|
+
*/
|
|
1600
|
+
function rollback(targetPath, silent) {
|
|
1601
|
+
try {
|
|
1602
|
+
const list = execSync('git stash list', { cwd: targetPath, stdio: 'pipe' }).toString();
|
|
1603
|
+
const match = list.match(/stash@\{(\d+)\}.*doorman-backup-before-fix/);
|
|
1604
|
+
if (!match) {
|
|
1605
|
+
if (!silent) {
|
|
1606
|
+
console.log(chalk.yellow(' No doorman backup found in git stash.'));
|
|
1607
|
+
}
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
execSync(`git stash pop stash@{${match[1]}}`, {
|
|
1611
|
+
cwd: targetPath,
|
|
1612
|
+
stdio: 'pipe',
|
|
1613
|
+
});
|
|
1614
|
+
if (!silent) {
|
|
1615
|
+
console.log(chalk.green(' Rolled back to doorman backup.'));
|
|
1616
|
+
}
|
|
1617
|
+
return true;
|
|
1618
|
+
} catch (e) {
|
|
1619
|
+
if (!silent) {
|
|
1620
|
+
console.log(chalk.red(` Rollback failed: ${e.message}`));
|
|
1621
|
+
}
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Apply a single fix to file content. Returns updated content or null on failure.
|
|
1628
|
+
*/
|
|
1629
|
+
function applyFix(content, fix) {
|
|
1630
|
+
if (fix.type === 'replace' && fix.old && fix.new) {
|
|
1631
|
+
if (content.includes(fix.old)) {
|
|
1632
|
+
return content.replace(fix.old, fix.new);
|
|
1633
|
+
}
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
if (fix.type === 'insert' && fix.content && fix.position !== undefined) {
|
|
1637
|
+
const lines = content.split('\n');
|
|
1638
|
+
lines.splice(fix.position, 0, fix.content);
|
|
1639
|
+
return lines.join('\n');
|
|
1640
|
+
}
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Tiered auto-fix system for Doorman.
|
|
1646
|
+
*
|
|
1647
|
+
* Tiers:
|
|
1648
|
+
* 1 (safe) — Config changes, headers, .gitignore, security files. Auto-applied.
|
|
1649
|
+
* 2 (review) — Code changes (parameterized queries, bcrypt). Shows diff, requires confirm.
|
|
1650
|
+
* 3 (manual) — Architecture changes. Shows instructions only.
|
|
1651
|
+
*
|
|
1652
|
+
* Usage:
|
|
1653
|
+
* doorman fix — dry-run (default), shows what would change
|
|
1654
|
+
* doorman fix --apply — actually apply fixes
|
|
1655
|
+
* doorman fix --undo — rollback the last backup
|
|
1656
|
+
*
|
|
1657
|
+
* @param {string} targetPath - Root path of the project to fix.
|
|
1658
|
+
* @param {object} options
|
|
1659
|
+
* @param {boolean} options.dryRun - Explicit dry-run flag (default behavior).
|
|
1660
|
+
* @param {boolean} options.apply - Actually apply fixes.
|
|
1661
|
+
* @param {boolean} options.undo - Rollback the last doorman stash backup.
|
|
1662
|
+
* @param {boolean} options.silent - Suppress output.
|
|
1663
|
+
*/
|
|
1664
|
+
export async function fix(targetPath, options = {}) {
|
|
1665
|
+
const silent = options.silent || false;
|
|
1666
|
+
const shouldApply = options.apply === true;
|
|
1667
|
+
const isDryRun = !shouldApply;
|
|
1668
|
+
|
|
1669
|
+
if (!silent) {
|
|
1670
|
+
console.log('');
|
|
1671
|
+
console.log(chalk.bold.cyan(' Doorman — Tiered Auto-Fix'));
|
|
1672
|
+
console.log('');
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Handle rollback
|
|
1676
|
+
if (options.undo) {
|
|
1677
|
+
if (!silent) console.log(chalk.gray(' Rolling back last doorman backup...'));
|
|
1678
|
+
const ok = rollback(targetPath, silent);
|
|
1679
|
+
if (!silent) console.log('');
|
|
1680
|
+
return { rolledBack: ok };
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Load cached scan results if available (instant), otherwise scan
|
|
1684
|
+
if (!silent) console.log(chalk.gray(' Loading scan results...'));
|
|
1685
|
+
let result;
|
|
1686
|
+
try {
|
|
1687
|
+
const { loadScanCache } = await import('./scan-cache.js');
|
|
1688
|
+
const cache = loadScanCache(targetPath);
|
|
1689
|
+
if (cache && cache.findings && cache.findings.length > 0) {
|
|
1690
|
+
result = { findings: cache.findings, score: cache.score, stack: cache.stack };
|
|
1691
|
+
if (!silent) console.log(chalk.gray(` Using cached scan (${cache.findings.length} findings from ${cache.timestamp.split('T')[0]})`));
|
|
1692
|
+
}
|
|
1693
|
+
} catch { /* ignore cache errors */ }
|
|
1694
|
+
|
|
1695
|
+
if (!result) {
|
|
1696
|
+
if (!silent) console.log(chalk.gray(' No cached scan found. Running scan...'));
|
|
1697
|
+
result = await check(targetPath, { ...options, silent: true });
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const fixable = result.findings.filter(f => f.fix);
|
|
1701
|
+
|
|
1702
|
+
// Load rules for verification (re-check findings after fix)
|
|
1703
|
+
let allRules = [];
|
|
1704
|
+
try {
|
|
1705
|
+
const { loadRules } = await import('./rules/index.js');
|
|
1706
|
+
allRules = loadRules({ category: 'security,bugs' });
|
|
1707
|
+
} catch { /* rules not available — skip verification */ }
|
|
1708
|
+
|
|
1709
|
+
if (fixable.length === 0) {
|
|
1710
|
+
if (!silent) {
|
|
1711
|
+
console.log(chalk.green.bold(' No auto-fixable issues found.'));
|
|
1712
|
+
console.log('');
|
|
1713
|
+
}
|
|
1714
|
+
return { fixed: 0, failed: 0, skipped: 0, findings: [] };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Classify by tier
|
|
1718
|
+
let tier1 = fixable.filter(f => getTier(f) === 1);
|
|
1719
|
+
let tier2 = fixable.filter(f => getTier(f) === 2);
|
|
1720
|
+
const tier3 = fixable.filter(f => getTier(f) === 3);
|
|
1721
|
+
|
|
1722
|
+
if (!silent) {
|
|
1723
|
+
console.log(chalk.bold(` Found ${fixable.length} fixable issue${fixable.length === 1 ? '' : 's'}`));
|
|
1724
|
+
if (tier3.length) console.log(chalk.gray(` + ${tier3.length} need manual changes`));
|
|
1725
|
+
console.log('');
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Dry-run mode (default): show what would change
|
|
1729
|
+
if (isDryRun) {
|
|
1730
|
+
if (!silent) {
|
|
1731
|
+
console.log(chalk.gray(' Dry-run mode (default). Use --apply to apply fixes.'));
|
|
1732
|
+
console.log('');
|
|
1733
|
+
|
|
1734
|
+
for (const finding of tier1) {
|
|
1735
|
+
console.log(chalk.green(` [safe] Would auto-fix: ${finding.title}`));
|
|
1736
|
+
if (finding.file) console.log(chalk.gray(` in ${finding.file}`));
|
|
1737
|
+
}
|
|
1738
|
+
for (const finding of tier2) {
|
|
1739
|
+
console.log(chalk.yellow(` [review] Would fix (with confirmation): ${finding.title}`));
|
|
1740
|
+
if (finding.file) console.log(chalk.gray(` in ${finding.file}`));
|
|
1741
|
+
if (finding.fix) console.log(formatDiff(finding.fix, finding.file || finding.fix.filePath || ''));
|
|
1742
|
+
}
|
|
1743
|
+
for (const finding of tier3) {
|
|
1744
|
+
console.log(chalk.red(` [manual] Requires manual change: ${finding.title}`));
|
|
1745
|
+
if (finding.fix?.instructions) {
|
|
1746
|
+
console.log(chalk.gray(` ${finding.fix.instructions}`));
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
console.log('');
|
|
1751
|
+
}
|
|
1752
|
+
return { dryRun: true, fixable: fixable.length, tier1: tier1.length, tier2: tier2.length, tier3: tier3.length };
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// --- Apply mode ---
|
|
1756
|
+
|
|
1757
|
+
// Collect unique files to be modified
|
|
1758
|
+
const filesToModify = new Set();
|
|
1759
|
+
for (const f of [...tier1, ...tier2]) {
|
|
1760
|
+
if (f.file) filesToModify.add(f.file);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// Safety: max file count
|
|
1764
|
+
if (filesToModify.size > MAX_FILES_PER_RUN) {
|
|
1765
|
+
if (!silent) {
|
|
1766
|
+
console.log(chalk.red(` Aborted: would modify ${filesToModify.size} files (limit is ${MAX_FILES_PER_RUN}).`));
|
|
1767
|
+
console.log('');
|
|
1768
|
+
}
|
|
1769
|
+
return { fixed: 0, failed: 0, aborted: true, reason: 'too many files' };
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Safety: check file sizes
|
|
1773
|
+
for (const file of filesToModify) {
|
|
1774
|
+
const fullPath = join(targetPath, file);
|
|
1775
|
+
const check = isSafeToModify(fullPath);
|
|
1776
|
+
if (!check.safe) {
|
|
1777
|
+
if (!silent) {
|
|
1778
|
+
console.log(chalk.red(` Skipping ${file}: ${check.reason}`));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Create git stash backup
|
|
1784
|
+
const backedUp = createBackup(targetPath);
|
|
1785
|
+
if (!silent) {
|
|
1786
|
+
if (backedUp) {
|
|
1787
|
+
console.log(chalk.green(' ✓ Backup created. Run `npx getdoorman fix --undo` if anything breaks.'));
|
|
1788
|
+
}
|
|
1789
|
+
console.log('');
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
let fixedCount = 0;
|
|
1793
|
+
let failedCount = 0;
|
|
1794
|
+
let skippedCount = 0;
|
|
1795
|
+
let verifiedCount = 0;
|
|
1796
|
+
const appliedFixes = [];
|
|
1797
|
+
const allFindings = [...tier1, ...tier2];
|
|
1798
|
+
const total = allFindings.length;
|
|
1799
|
+
let current = 0;
|
|
1800
|
+
const maxFixes = options.maxFixes || Infinity;
|
|
1801
|
+
let creditLimitHit = false;
|
|
1802
|
+
|
|
1803
|
+
// --- Apply registry-based fixes per file ---
|
|
1804
|
+
// Group all fixable findings by file
|
|
1805
|
+
const findingsByFile = {};
|
|
1806
|
+
const findingsNoFile = [];
|
|
1807
|
+
for (const finding of allFindings) {
|
|
1808
|
+
if (finding.file) {
|
|
1809
|
+
if (!findingsByFile[finding.file]) findingsByFile[finding.file] = [];
|
|
1810
|
+
findingsByFile[finding.file].push(finding);
|
|
1811
|
+
} else {
|
|
1812
|
+
findingsNoFile.push(finding);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
if (!silent && allFindings.length > 0) {
|
|
1817
|
+
console.log(chalk.green.bold(' Applying fixes:'));
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
for (const [file, findings] of Object.entries(findingsByFile)) {
|
|
1821
|
+
const fullPath = join(targetPath, file);
|
|
1822
|
+
const sizeCheck = isSafeToModify(fullPath);
|
|
1823
|
+
if (!sizeCheck.safe) {
|
|
1824
|
+
skippedCount += findings.length;
|
|
1825
|
+
current += findings.length;
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
try {
|
|
1830
|
+
let content = readFileSync(fullPath, 'utf-8');
|
|
1831
|
+
const original = content;
|
|
1832
|
+
|
|
1833
|
+
// For each finding, try to fix it and verify the fix works
|
|
1834
|
+
for (const finding of findings) {
|
|
1835
|
+
current++;
|
|
1836
|
+
if (fixedCount >= maxFixes) { creditLimitHit = true; skippedCount++; continue; }
|
|
1837
|
+
|
|
1838
|
+
const beforeFix = content;
|
|
1839
|
+
|
|
1840
|
+
// Try 1: finding-level fix (type: replace with old/new)
|
|
1841
|
+
let attempted = false;
|
|
1842
|
+
if (finding.fix && typeof finding.fix === 'object' && finding.fix.type === 'replace') {
|
|
1843
|
+
const updated = applyFix(content, finding.fix);
|
|
1844
|
+
if (updated !== null) { content = updated; attempted = true; }
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Try 2: registry pattern fix (run all, take the diff)
|
|
1848
|
+
if (!attempted) {
|
|
1849
|
+
const { content: regFixed, applied } = applyAllRegistryFixes(content, file, registryEntries);
|
|
1850
|
+
if (applied.length > 0) { content = regFixed; attempted = true; }
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Try 3: inline pattern fix
|
|
1854
|
+
if (!attempted) {
|
|
1855
|
+
const fixId = finding.fix && typeof finding.fix === 'string' ? finding.fix : null;
|
|
1856
|
+
const pfn = fixId ? patternFixes[fixId] : null;
|
|
1857
|
+
if (pfn) {
|
|
1858
|
+
const updated = pfn.fn(content);
|
|
1859
|
+
if (updated !== null) { content = updated; attempted = true; }
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Verify: did the fix actually resolve the finding?
|
|
1864
|
+
if (attempted && content !== beforeFix) {
|
|
1865
|
+
// Re-run the specific rule on the new content to verify
|
|
1866
|
+
let stillDetected = false;
|
|
1867
|
+
if (finding.ruleId) {
|
|
1868
|
+
try {
|
|
1869
|
+
const tempFiles = new Map([[file, content]]);
|
|
1870
|
+
const rule = allRules.find(r => r.id === finding.ruleId);
|
|
1871
|
+
if (rule) {
|
|
1872
|
+
const recheck = rule.check({ files: tempFiles, stack: result.stack || {}, silent: true });
|
|
1873
|
+
const recheckArr = Array.isArray(recheck) ? recheck : [];
|
|
1874
|
+
// Check if same finding still exists on same line
|
|
1875
|
+
stillDetected = recheckArr.some(f =>
|
|
1876
|
+
f.file === file && (!finding.line || f.line === finding.line)
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
} catch { /* rule threw — assume fixed */ }
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (!stillDetected) {
|
|
1883
|
+
fixedCount++;
|
|
1884
|
+
if (!silent) {
|
|
1885
|
+
console.log(chalk.green(` ✓ ${finding.title}`));
|
|
1886
|
+
console.log(chalk.gray(` in ${file}`));
|
|
1887
|
+
}
|
|
1888
|
+
} else {
|
|
1889
|
+
// Fix changed the file but didn't resolve the finding — rollback
|
|
1890
|
+
content = beforeFix;
|
|
1891
|
+
}
|
|
1892
|
+
} else if (attempted) {
|
|
1893
|
+
content = beforeFix;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Write if content changed — with syntax verification
|
|
1898
|
+
if (content !== original) {
|
|
1899
|
+
// Verify the fixed code still parses before writing
|
|
1900
|
+
if (isSyntaxValid(fullPath, content)) {
|
|
1901
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
1902
|
+
} else {
|
|
1903
|
+
// Syntax broken — rollback this file, don't count as fixed
|
|
1904
|
+
if (!silent) {
|
|
1905
|
+
console.log(chalk.red(` ✗ Fix broke syntax in ${file} — rolled back`));
|
|
1906
|
+
}
|
|
1907
|
+
fixedCount -= findings.filter(f => f._fixed).length;
|
|
1908
|
+
content = original; // don't write
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// Verify fixes
|
|
1913
|
+
for (const finding of findings) {
|
|
1914
|
+
if (finding.fix && typeof finding.fix === 'object') {
|
|
1915
|
+
if (verifyFix(fullPath, finding.fix)) verifiedCount++;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
failedCount += findings.length;
|
|
1920
|
+
current += findings.length;
|
|
1921
|
+
if (!silent) console.log(chalk.red(` ✗ Failed to apply fixes for ${relativePath}: ${e.message}`));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// File-creation fixes (no source file)
|
|
1926
|
+
for (const finding of findingsNoFile) {
|
|
1927
|
+
current++;
|
|
1928
|
+
if (finding.fix?.type === 'create' && finding.fix.filePath && finding.fix.content) {
|
|
1929
|
+
try {
|
|
1930
|
+
const createPath = join(targetPath, finding.fix.filePath);
|
|
1931
|
+
writeFileSync(createPath, finding.fix.content, 'utf-8');
|
|
1932
|
+
appliedFixes.push(finding.fix);
|
|
1933
|
+
fixedCount++;
|
|
1934
|
+
if (!silent) console.log(chalk.green(` ${progress(current, total, '')} ✓ ${finding.title} (created ${finding.fix.filePath})`));
|
|
1935
|
+
verifiedCount++;
|
|
1936
|
+
} catch {
|
|
1937
|
+
failedCount++;
|
|
1938
|
+
if (!silent) console.log(chalk.red(` ${progress(current, total, '')} ✗ ${finding.title} (create failed)`));
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// --- AI Fix: try AI for findings regex couldn't fix ---
|
|
1944
|
+
const hasApiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
|
|
1945
|
+
if (hasApiKey && shouldApply && failedCount > 0 && fixedCount < maxFixes) {
|
|
1946
|
+
try {
|
|
1947
|
+
const { aiFixAll } = await import('./ai-fixer.js');
|
|
1948
|
+
const { resolve: resolvePath } = await import('path');
|
|
1949
|
+
const resolvedPath = resolvePath(targetPath);
|
|
1950
|
+
|
|
1951
|
+
// Collect unfixed findings that have files
|
|
1952
|
+
const unfixed = allFindings.filter(f => f.file && !f._regexFixed).slice(0, Math.min(10, maxFixes - fixedCount));
|
|
1953
|
+
|
|
1954
|
+
if (unfixed.length > 0) {
|
|
1955
|
+
if (!silent) {
|
|
1956
|
+
console.log('');
|
|
1957
|
+
console.log(chalk.cyan(' Trying AI fixes for remaining issues...'));
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// Build file content map
|
|
1961
|
+
const fileContentMap = {};
|
|
1962
|
+
for (const f of unfixed) {
|
|
1963
|
+
if (f.file && !fileContentMap[f.file]) {
|
|
1964
|
+
try { fileContentMap[f.file] = readFileSync(join(targetPath, f.file), 'utf-8'); } catch {}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
const aiResults = await aiFixAll(unfixed, fileContentMap, { cwd: resolvedPath });
|
|
1969
|
+
|
|
1970
|
+
for (const { finding, fix: aiFix } of aiResults) {
|
|
1971
|
+
if (fixedCount >= maxFixes) break;
|
|
1972
|
+
if (!aiFix || !aiFix.old || !aiFix.new || !finding.file) continue;
|
|
1973
|
+
|
|
1974
|
+
const fullPath = join(targetPath, finding.file);
|
|
1975
|
+
try {
|
|
1976
|
+
let content = readFileSync(fullPath, 'utf-8');
|
|
1977
|
+
if (content.includes(aiFix.old)) {
|
|
1978
|
+
const newContent = content.replace(aiFix.old, aiFix.new);
|
|
1979
|
+
if (isSyntaxValid(fullPath, newContent)) {
|
|
1980
|
+
writeFileSync(fullPath, newContent, 'utf-8');
|
|
1981
|
+
fixedCount++;
|
|
1982
|
+
if (!silent) {
|
|
1983
|
+
console.log(chalk.green(` ✓ ${finding.title}`));
|
|
1984
|
+
console.log(chalk.gray(` in ${finding.file} (AI fix)`));
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
} catch {}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
} catch {
|
|
1992
|
+
// AI fixer not available or failed — continue silently
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// --- Tier 3: manual instructions only ---
|
|
1997
|
+
if (tier3.length > 0 && !silent) {
|
|
1998
|
+
console.log('');
|
|
1999
|
+
console.log(chalk.red.bold(' Tier 3 — Manual changes required:'));
|
|
2000
|
+
for (const finding of tier3) {
|
|
2001
|
+
console.log('');
|
|
2002
|
+
console.log(chalk.red(` ⚠ ${finding.title}`));
|
|
2003
|
+
if (finding.file) console.log(chalk.gray(` File: ${finding.file}`));
|
|
2004
|
+
if (finding.fix?.instructions) {
|
|
2005
|
+
console.log(chalk.white(` Instructions: ${finding.fix.instructions}`));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
skippedCount += tier3.length;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// --- Summary ---
|
|
2012
|
+
const { added, removed } = countLinesChanged(appliedFixes);
|
|
2013
|
+
|
|
2014
|
+
// Estimate new score from cached findings minus what we fixed
|
|
2015
|
+
let postScore = null;
|
|
2016
|
+
let remainingFixable = 0;
|
|
2017
|
+
if (fixedCount > 0 && shouldApply) {
|
|
2018
|
+
try {
|
|
2019
|
+
// Load the cached scan and remove findings we fixed
|
|
2020
|
+
const { loadScanCache, saveScanCache } = await import('./scan-cache.js');
|
|
2021
|
+
const cache = loadScanCache(targetPath);
|
|
2022
|
+
if (cache && cache.findings) {
|
|
2023
|
+
// Remove fixed findings (match by ruleId + file)
|
|
2024
|
+
const fixedKeys = new Set();
|
|
2025
|
+
for (const [file, findings] of Object.entries(findingsByFile)) {
|
|
2026
|
+
for (const f of findings) {
|
|
2027
|
+
fixedKeys.add(`${f.ruleId}:${f.file}:${f.line}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
const remaining = cache.findings.filter(f => {
|
|
2031
|
+
const key = `${f.ruleId}:${f.file}:${f.line}`;
|
|
2032
|
+
return !fixedKeys.has(key);
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
postScore = (await import('./reporter.js')).calculateScore(remaining);
|
|
2036
|
+
remainingFixable = remaining.filter(f => f.fix).length;
|
|
2037
|
+
|
|
2038
|
+
// Update cache with remaining findings
|
|
2039
|
+
saveScanCache(targetPath, remaining, cache.stack, postScore);
|
|
2040
|
+
}
|
|
2041
|
+
} catch {
|
|
2042
|
+
// Non-critical
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (!silent) {
|
|
2047
|
+
console.log('');
|
|
2048
|
+
console.log(chalk.bold(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
2049
|
+
console.log('');
|
|
2050
|
+
|
|
2051
|
+
if (fixedCount > 0) {
|
|
2052
|
+
console.log(chalk.green.bold(` ✓ ${fixedCount} issue${fixedCount === 1 ? '' : 's'} fixed`));
|
|
2053
|
+
if (postScore !== null) {
|
|
2054
|
+
const scoreColor = postScore >= 70 ? chalk.green.bold : postScore >= 40 ? chalk.yellow.bold : chalk.red.bold;
|
|
2055
|
+
console.log(` Score: ${scoreColor(`${postScore}/100`)}`);
|
|
2056
|
+
}
|
|
2057
|
+
} else {
|
|
2058
|
+
console.log(chalk.dim(' No fixes applied this run.'));
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
if (remainingFixable > 0) {
|
|
2062
|
+
console.log('');
|
|
2063
|
+
console.log(chalk.cyan(` ${remainingFixable} more issue${remainingFixable === 1 ? '' : 's'} can be fixed with AI Fix (Pro plan).`));
|
|
2064
|
+
console.log(chalk.gray(' Upgrade at https://doorman.sh/pro'));
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (tier3.length > 0) {
|
|
2068
|
+
console.log('');
|
|
2069
|
+
console.log(chalk.dim(` ${tier3.length} issue${tier3.length === 1 ? '' : 's'} need manual review (run --detail to see).`));
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (backedUp) {
|
|
2073
|
+
console.log('');
|
|
2074
|
+
console.log(chalk.gray(' Backup saved. Run `npx getdoorman fix --undo` to rollback.'));
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
console.log('');
|
|
2078
|
+
console.log(chalk.bold(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
2079
|
+
console.log('');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
recordFix(targetPath, { applied: fixedCount, offered: fixable.length, accepted: fixedCount });
|
|
2083
|
+
|
|
2084
|
+
// Generate unified diff output for piping to git apply
|
|
2085
|
+
if (isDryRun && options.diff && appliedFixes.length > 0) {
|
|
2086
|
+
console.log('');
|
|
2087
|
+
console.log(chalk.bold(' Unified diff (pipe to `git apply`):'));
|
|
2088
|
+
console.log('');
|
|
2089
|
+
for (const fix of appliedFixes) {
|
|
2090
|
+
if (fix.original && fix.modified && fix.filePath) {
|
|
2091
|
+
const orig = fix.original.split('\n');
|
|
2092
|
+
const mod = fix.modified.split('\n');
|
|
2093
|
+
console.log(`--- a/${fix.filePath}`);
|
|
2094
|
+
console.log(`+++ b/${fix.filePath}`);
|
|
2095
|
+
// Simple line-by-line diff
|
|
2096
|
+
const maxLen = Math.max(orig.length, mod.length);
|
|
2097
|
+
let hunkStart = -1;
|
|
2098
|
+
for (let i = 0; i < maxLen; i++) {
|
|
2099
|
+
if (orig[i] !== mod[i]) {
|
|
2100
|
+
if (hunkStart < 0) {
|
|
2101
|
+
hunkStart = Math.max(0, i - 2);
|
|
2102
|
+
console.log(`@@ -${hunkStart + 1},${orig.length} +${hunkStart + 1},${mod.length} @@`);
|
|
2103
|
+
}
|
|
2104
|
+
if (i < orig.length && orig[i] !== undefined) console.log(`-${orig[i]}`);
|
|
2105
|
+
if (i < mod.length && mod[i] !== undefined) console.log(`+${mod[i]}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
return { fixed: fixedCount, failed: failedCount, skipped: skippedCount, verified: verifiedCount, linesAdded: added, linesRemoved: removed };
|
|
2113
|
+
}
|