faultmesh 1.0.0 → 1.1.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/README.md +61 -19
- package/dist/dashboard/app.js +1906 -139
- package/dist/dashboard/index.html +393 -99
- package/dist/dashboard/styles.css +1191 -23
- package/dist/engine/ControlApi.d.ts +13 -1
- package/dist/engine/ControlApi.js +304 -14
- package/dist/engine/FaultMeshProxy.d.ts +2 -0
- package/dist/engine/FaultMeshProxy.js +41 -5
- package/dist/engine/TelemetryHub.d.ts +1 -0
- package/dist/engine/TelemetryHub.js +3 -0
- package/dist/engine/ToxicPipeline.d.ts +5 -4
- package/dist/engine/ToxicPipeline.js +17 -4
- package/dist/healer/AiHealer.d.ts +30 -0
- package/dist/healer/AiHealer.js +188 -0
- package/dist/healer/AutoHealer.d.ts +24 -0
- package/dist/healer/AutoHealer.js +503 -0
- package/dist/healer/DiffGenerator.d.ts +10 -0
- package/dist/healer/DiffGenerator.js +63 -0
- package/dist/healer/DiffUtil.d.ts +6 -0
- package/dist/healer/DiffUtil.js +67 -0
- package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
- package/dist/healer/transformers/ExpressTransformers.js +228 -0
- package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
- package/dist/healer/transformers/FastApiTransformers.js +93 -0
- package/dist/healer/transformers/GoTransformers.d.ts +19 -0
- package/dist/healer/transformers/GoTransformers.js +106 -0
- package/dist/healer/types.d.ts +59 -0
- package/dist/healer/types.js +1 -0
- package/dist/redteam/EccAgentBridge.d.ts +20 -0
- package/dist/redteam/EccAgentBridge.js +303 -0
- package/dist/redteam/RedTeamEngine.d.ts +56 -0
- package/dist/redteam/RedTeamEngine.js +709 -0
- package/dist/redteam/types.d.ts +117 -0
- package/dist/redteam/types.js +5 -0
- package/dist/scorer/ResilienceScorer.d.ts +5 -0
- package/dist/scorer/ResilienceScorer.js +323 -7
- package/dist/scorer/SecurityAuditor.d.ts +8 -0
- package/dist/scorer/SecurityAuditor.js +471 -69
- package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
- package/dist/scorer/TrafficStormAuditor.js +328 -69
- package/dist/server.js +17 -10
- package/dist/types.d.ts +7 -3
- package/package.json +2 -1
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DiffGenerator } from './DiffGenerator.js';
|
|
4
|
+
import { ExpressTransformers } from './transformers/ExpressTransformers.js';
|
|
5
|
+
import { FastApiTransformers } from './transformers/FastApiTransformers.js';
|
|
6
|
+
import { GoTransformers } from './transformers/GoTransformers.js';
|
|
7
|
+
import { AiHealer } from './AiHealer.js';
|
|
8
|
+
export class AutoHealer {
|
|
9
|
+
static BLACKLISTED_NAMES = [
|
|
10
|
+
'.git',
|
|
11
|
+
'node_modules',
|
|
12
|
+
'venv',
|
|
13
|
+
'.env',
|
|
14
|
+
'.env.local',
|
|
15
|
+
'.env.production',
|
|
16
|
+
'.env.development',
|
|
17
|
+
'.DS_Store',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Scan a target project directory and generate reviewable remediation patches
|
|
21
|
+
*/
|
|
22
|
+
static async scan(options) {
|
|
23
|
+
const rawDir = options.projectDir || '.';
|
|
24
|
+
const resolvedDir = path.resolve(rawDir);
|
|
25
|
+
// 1. Path Safety & Confinement Validation
|
|
26
|
+
const rootValidation = this.validateProjectDir(resolvedDir);
|
|
27
|
+
if (!rootValidation.valid) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
framework: 'generic-node',
|
|
31
|
+
projectRoot: resolvedDir,
|
|
32
|
+
patches: [],
|
|
33
|
+
warnings: [rootValidation.error || 'Invalid directory path'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// 2. Framework Detection
|
|
37
|
+
const { framework, entryFile, warnings } = this.detectFrameworkAndEntry(resolvedDir);
|
|
38
|
+
if (!entryFile) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
framework,
|
|
42
|
+
projectRoot: resolvedDir,
|
|
43
|
+
patches: [],
|
|
44
|
+
warnings: [...warnings, 'Could not locate server entry file (e.g. server.js, index.ts, main.py, main.go, main.rs)'],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const fullEntryPath = path.resolve(resolvedDir, entryFile);
|
|
48
|
+
if (!fs.existsSync(fullEntryPath)) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
framework,
|
|
52
|
+
projectRoot: resolvedDir,
|
|
53
|
+
entryFile,
|
|
54
|
+
patches: [],
|
|
55
|
+
warnings: [...warnings, `Entry file does not exist: ${entryFile}`],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const originalContent = fs.readFileSync(fullEntryPath, 'utf8');
|
|
59
|
+
const patches = [];
|
|
60
|
+
const checksToEvaluate = options.failedChecks && options.failedChecks.length > 0
|
|
61
|
+
? options.failedChecks
|
|
62
|
+
: [
|
|
63
|
+
'Defensive Security Headers',
|
|
64
|
+
'Oversized Payload & Buffer OOM Defense (HTTP 413)',
|
|
65
|
+
'CORS & Origin Validation',
|
|
66
|
+
'Error Sanitization & Stack Trace Exposure',
|
|
67
|
+
'Slowloris Connection Drip Defense',
|
|
68
|
+
'Host Header Poisoning & Reflection',
|
|
69
|
+
'Path Traversal & Directory Escape (../)',
|
|
70
|
+
'Duplicate Request Idempotency Protection',
|
|
71
|
+
];
|
|
72
|
+
// Check AI availability for universal multi-language and arbitrary error remediation
|
|
73
|
+
const aiAvailable = await AiHealer.isAvailable(options.aiConfig);
|
|
74
|
+
let engineUsed = 'codemod';
|
|
75
|
+
// 3. Apply Transformations
|
|
76
|
+
let workingContent = originalContent;
|
|
77
|
+
for (const check of checksToEvaluate) {
|
|
78
|
+
const lowerCheck = check.toLowerCase();
|
|
79
|
+
let res = null;
|
|
80
|
+
let usedAiForPatch = false;
|
|
81
|
+
// Tier 1: Deterministic CodeMod Transformers
|
|
82
|
+
if (options.engineMode !== 'ai') {
|
|
83
|
+
if (framework === 'express' || framework === 'generic-node') {
|
|
84
|
+
if (lowerCheck.includes('host')) {
|
|
85
|
+
res = ExpressTransformers.applyHostHeaderValidation(workingContent);
|
|
86
|
+
}
|
|
87
|
+
else if (lowerCheck.includes('traversal') || lowerCheck.includes('directory escape') || lowerCheck.includes('path')) {
|
|
88
|
+
res = ExpressTransformers.applyPathTraversalGuard(workingContent);
|
|
89
|
+
}
|
|
90
|
+
else if (lowerCheck.includes('idempotency') || lowerCheck.includes('concurrency') || lowerCheck.includes('race condition') || lowerCheck.includes('race-condition') || lowerCheck.includes('duplicate request') || lowerCheck.includes('state mutation')) {
|
|
91
|
+
res = ExpressTransformers.applyIdempotencyProtection(workingContent);
|
|
92
|
+
}
|
|
93
|
+
else if (lowerCheck.includes('header') || lowerCheck.includes('nosniff') || lowerCheck.includes('defensive')) {
|
|
94
|
+
res = ExpressTransformers.applyDefensiveHeaders(workingContent);
|
|
95
|
+
}
|
|
96
|
+
else if (lowerCheck.includes('payload') || lowerCheck.includes('413') || lowerCheck.includes('oom')) {
|
|
97
|
+
res = ExpressTransformers.applyPayloadLimit(workingContent);
|
|
98
|
+
}
|
|
99
|
+
else if (lowerCheck.includes('cors') || lowerCheck.includes('origin')) {
|
|
100
|
+
res = ExpressTransformers.applyCorsLockdown(workingContent);
|
|
101
|
+
}
|
|
102
|
+
else if (lowerCheck.includes('error') || lowerCheck.includes('stack') || lowerCheck.includes('sanitization')) {
|
|
103
|
+
res = ExpressTransformers.applyErrorSanitization(workingContent);
|
|
104
|
+
}
|
|
105
|
+
else if (lowerCheck.includes('slowloris') || lowerCheck.includes('drip') || lowerCheck.includes('timeout')) {
|
|
106
|
+
res = ExpressTransformers.applySocketTimeouts(workingContent);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (framework === 'fastapi' || framework === 'generic-python') {
|
|
110
|
+
if (lowerCheck.includes('header') || lowerCheck.includes('nosniff') || lowerCheck.includes('defensive')) {
|
|
111
|
+
res = FastApiTransformers.applyDefensiveHeaders(workingContent);
|
|
112
|
+
}
|
|
113
|
+
else if (lowerCheck.includes('payload') || lowerCheck.includes('413') || lowerCheck.includes('oom')) {
|
|
114
|
+
res = FastApiTransformers.applyPayloadLimit(workingContent);
|
|
115
|
+
}
|
|
116
|
+
else if (lowerCheck.includes('cors') || lowerCheck.includes('origin')) {
|
|
117
|
+
res = FastApiTransformers.applyCorsLockdown(workingContent);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (framework === 'go-gin' || framework === 'go-nethttp' || framework === 'generic-go') {
|
|
121
|
+
if (lowerCheck.includes('header') || lowerCheck.includes('nosniff') || lowerCheck.includes('defensive')) {
|
|
122
|
+
res = GoTransformers.applyDefensiveHeaders(workingContent);
|
|
123
|
+
}
|
|
124
|
+
else if (lowerCheck.includes('payload') || lowerCheck.includes('413') || lowerCheck.includes('oom')) {
|
|
125
|
+
res = GoTransformers.applyPayloadLimit(workingContent);
|
|
126
|
+
}
|
|
127
|
+
else if (lowerCheck.includes('slowloris') || lowerCheck.includes('drip') || lowerCheck.includes('timeout')) {
|
|
128
|
+
res = GoTransformers.applyServerTimeouts(workingContent);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Tier 2: AI Healer Agent (Universal Multi-Language & Arbitrary Error Remediation)
|
|
133
|
+
if ((!res || !res.modified) && (options.engineMode === 'ai' || options.engineMode === 'hybrid' || aiAvailable.available)) {
|
|
134
|
+
const aiResult = await AiHealer.generateRemediation({
|
|
135
|
+
filePath: entryFile,
|
|
136
|
+
originalContent: workingContent,
|
|
137
|
+
framework,
|
|
138
|
+
failedChecks: [check],
|
|
139
|
+
config: options.aiConfig,
|
|
140
|
+
});
|
|
141
|
+
if (aiResult.success && aiResult.content !== workingContent) {
|
|
142
|
+
res = {
|
|
143
|
+
modified: true,
|
|
144
|
+
content: aiResult.content,
|
|
145
|
+
description: aiResult.description,
|
|
146
|
+
};
|
|
147
|
+
usedAiForPatch = true;
|
|
148
|
+
engineUsed = engineUsed === 'codemod' ? 'ai-agent' : 'hybrid';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (res && res.modified) {
|
|
152
|
+
const cleanKey = check.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
153
|
+
const patchId = `patch_${cleanKey}`;
|
|
154
|
+
const diff = DiffGenerator.generateDiff(entryFile, workingContent, res.content);
|
|
155
|
+
patches.push({
|
|
156
|
+
id: patchId,
|
|
157
|
+
checkName: check,
|
|
158
|
+
filePath: fullEntryPath,
|
|
159
|
+
relativePath: entryFile,
|
|
160
|
+
originalContent: workingContent,
|
|
161
|
+
remediatedContent: res.content,
|
|
162
|
+
diff,
|
|
163
|
+
description: res.description,
|
|
164
|
+
framework,
|
|
165
|
+
engine: usedAiForPatch ? 'ai-agent' : 'codemod',
|
|
166
|
+
});
|
|
167
|
+
// Update workingContent for sequential compounding remediations
|
|
168
|
+
workingContent = res.content;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
framework,
|
|
174
|
+
projectRoot: resolvedDir,
|
|
175
|
+
entryFile,
|
|
176
|
+
patches,
|
|
177
|
+
warnings,
|
|
178
|
+
engineUsed,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Safely applies generated remediation patches to disk with automatic backup
|
|
183
|
+
*/
|
|
184
|
+
static async apply(options) {
|
|
185
|
+
const rawDir = options.projectDir || '.';
|
|
186
|
+
const resolvedDir = path.resolve(rawDir);
|
|
187
|
+
const rootValidation = this.validateProjectDir(resolvedDir);
|
|
188
|
+
if (!rootValidation.valid) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
appliedCount: 0,
|
|
192
|
+
appliedPatches: [],
|
|
193
|
+
errors: [rootValidation.error || 'Invalid project directory'],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Run a fresh scan to generate latest patches
|
|
197
|
+
const scanResult = await this.scan({
|
|
198
|
+
projectDir: resolvedDir,
|
|
199
|
+
failedChecks: options.failedChecks,
|
|
200
|
+
aiConfig: options.aiConfig,
|
|
201
|
+
engineMode: options.engineMode,
|
|
202
|
+
});
|
|
203
|
+
if (!scanResult.success || scanResult.patches.length === 0) {
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
appliedCount: 0,
|
|
207
|
+
appliedPatches: [],
|
|
208
|
+
errors: scanResult.warnings.length > 0 ? scanResult.warnings : ['No applicable patches found'],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
let patchesToApply = scanResult.patches;
|
|
212
|
+
if (options.patchIds && options.patchIds.length > 0) {
|
|
213
|
+
const filtered = scanResult.patches.filter(p => options.patchIds.includes(p.id) ||
|
|
214
|
+
options.patchIds.some(id => id.toLowerCase().includes(p.checkName.toLowerCase()) ||
|
|
215
|
+
p.id.toLowerCase().includes(id.toLowerCase())));
|
|
216
|
+
if (filtered.length > 0) {
|
|
217
|
+
patchesToApply = filtered;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (patchesToApply.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
appliedCount: 0,
|
|
224
|
+
appliedPatches: [],
|
|
225
|
+
errors: ['No patches matched criteria'],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Prepare backup directory
|
|
229
|
+
let backupDir;
|
|
230
|
+
if (options.createBackup !== false) {
|
|
231
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
232
|
+
backupDir = path.join(resolvedDir, '.faultmesh-backup', timestamp);
|
|
233
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
const appliedPatches = [];
|
|
236
|
+
const errors = [];
|
|
237
|
+
// Group patches by file so we write final aggregated content
|
|
238
|
+
const filesMap = new Map();
|
|
239
|
+
for (const patch of patchesToApply) {
|
|
240
|
+
filesMap.set(patch.filePath, patch.remediatedContent);
|
|
241
|
+
appliedPatches.push(patch.id);
|
|
242
|
+
}
|
|
243
|
+
for (const [filePath, content] of filesMap.entries()) {
|
|
244
|
+
try {
|
|
245
|
+
if (backupDir && fs.existsSync(filePath)) {
|
|
246
|
+
const rel = path.relative(resolvedDir, filePath);
|
|
247
|
+
const backupFilePath = path.join(backupDir, rel);
|
|
248
|
+
fs.mkdirSync(path.dirname(backupFilePath), { recursive: true });
|
|
249
|
+
fs.copyFileSync(filePath, backupFilePath);
|
|
250
|
+
}
|
|
251
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
errors.push(`Failed to write patch to ${filePath}: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
success: errors.length === 0,
|
|
259
|
+
appliedCount: appliedPatches.length,
|
|
260
|
+
appliedPatches,
|
|
261
|
+
backupDir,
|
|
262
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Restores files from a previously created .faultmesh-backup directory
|
|
267
|
+
*/
|
|
268
|
+
static async rollback(options) {
|
|
269
|
+
const projectDir = path.resolve(options.projectDir);
|
|
270
|
+
const backupDir = path.resolve(options.backupDir);
|
|
271
|
+
if (!fs.existsSync(backupDir)) {
|
|
272
|
+
return { success: false, restoredFiles: [], errors: ['Backup directory does not exist'] };
|
|
273
|
+
}
|
|
274
|
+
const restoredFiles = [];
|
|
275
|
+
const errors = [];
|
|
276
|
+
const restoreRecursive = (currentDir) => {
|
|
277
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const fullSource = path.join(currentDir, entry.name);
|
|
280
|
+
if (entry.isDirectory()) {
|
|
281
|
+
restoreRecursive(fullSource);
|
|
282
|
+
}
|
|
283
|
+
else if (entry.isFile()) {
|
|
284
|
+
const rel = path.relative(backupDir, fullSource);
|
|
285
|
+
const destPath = path.join(projectDir, rel);
|
|
286
|
+
try {
|
|
287
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
288
|
+
fs.copyFileSync(fullSource, destPath);
|
|
289
|
+
restoredFiles.push(rel);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
errors.push(`Failed restoring ${rel}: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
restoreRecursive(backupDir);
|
|
298
|
+
return {
|
|
299
|
+
success: errors.length === 0,
|
|
300
|
+
restoredFiles,
|
|
301
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Validates target directory exists and prevents targeting system root or parent escapes
|
|
306
|
+
*/
|
|
307
|
+
static validateProjectDir(targetDir) {
|
|
308
|
+
if (!fs.existsSync(targetDir)) {
|
|
309
|
+
return { valid: false, error: `Target project directory does not exist: ${targetDir}` };
|
|
310
|
+
}
|
|
311
|
+
const stat = fs.statSync(targetDir);
|
|
312
|
+
if (!stat.isDirectory()) {
|
|
313
|
+
return { valid: false, error: `Target path is not a directory: ${targetDir}` };
|
|
314
|
+
}
|
|
315
|
+
// Block root directories (e.g. C:\, C:\Windows, /)
|
|
316
|
+
const parsed = path.parse(targetDir);
|
|
317
|
+
if (parsed.root === targetDir) {
|
|
318
|
+
return { valid: false, error: 'Cannot target the operating system root drive' };
|
|
319
|
+
}
|
|
320
|
+
const lower = targetDir.toLowerCase();
|
|
321
|
+
if (lower.includes('\\windows') || lower.includes('/system') || lower.includes('/etc')) {
|
|
322
|
+
return { valid: false, error: 'Access to system directories is strictly prohibited' };
|
|
323
|
+
}
|
|
324
|
+
return { valid: true };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Detects project backend framework and primary entry file
|
|
328
|
+
*/
|
|
329
|
+
static detectFrameworkAndEntry(projectDir) {
|
|
330
|
+
const warnings = [];
|
|
331
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
332
|
+
const reqPath = path.join(projectDir, 'requirements.txt');
|
|
333
|
+
const pyprojectPath = path.join(projectDir, 'pyproject.toml');
|
|
334
|
+
let framework = 'generic-node';
|
|
335
|
+
// 1. Node.js detection
|
|
336
|
+
if (fs.existsSync(pkgPath)) {
|
|
337
|
+
try {
|
|
338
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
339
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
340
|
+
if (allDeps.express) {
|
|
341
|
+
framework = 'express';
|
|
342
|
+
}
|
|
343
|
+
else if (allDeps.fastify) {
|
|
344
|
+
framework = 'fastify';
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
framework = 'generic-node';
|
|
348
|
+
}
|
|
349
|
+
// If scanning FaultMesh workspace root itself, prioritize targeting the sample backend and prevent targeting FaultMesh runtime
|
|
350
|
+
if (pkg.name === 'faultmesh' || fs.existsSync(path.join(projectDir, 'examples', 'vulnerable-backend', 'server.js'))) {
|
|
351
|
+
if (fs.existsSync(path.join(projectDir, 'examples', 'vulnerable-backend', 'server.js'))) {
|
|
352
|
+
return { framework: 'express', entryFile: 'examples/vulnerable-backend/server.js', warnings };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Check package.json "main" (excluding compiled build outputs like dist/)
|
|
356
|
+
const isBuildOutput = typeof pkg.main === 'string' && (pkg.main.startsWith('dist/') || pkg.main.startsWith('build/') || pkg.main.startsWith('out/'));
|
|
357
|
+
if (pkg.main && !isBuildOutput && fs.existsSync(path.join(projectDir, pkg.main))) {
|
|
358
|
+
return { framework, entryFile: pkg.main, warnings };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
warnings.push(`Warning: Unable to parse package.json: ${err.message}`);
|
|
363
|
+
}
|
|
364
|
+
// Check common node entry points
|
|
365
|
+
const commonNodeEntries = [
|
|
366
|
+
'server.ts', 'server.js',
|
|
367
|
+
'src/server.ts', 'src/server.js',
|
|
368
|
+
'index.ts', 'index.js',
|
|
369
|
+
'src/index.ts', 'src/index.js',
|
|
370
|
+
'app.ts', 'app.js',
|
|
371
|
+
'src/app.ts', 'src/app.js',
|
|
372
|
+
];
|
|
373
|
+
for (const entry of commonNodeEntries) {
|
|
374
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
375
|
+
return { framework, entryFile: entry, warnings };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// 2. Python detection
|
|
380
|
+
if (fs.existsSync(reqPath) || fs.existsSync(pyprojectPath)) {
|
|
381
|
+
let isFastApi = false;
|
|
382
|
+
let isFlask = false;
|
|
383
|
+
let isDjango = false;
|
|
384
|
+
const checkPyManifest = (content) => {
|
|
385
|
+
if (content.includes('fastapi'))
|
|
386
|
+
isFastApi = true;
|
|
387
|
+
if (content.includes('flask'))
|
|
388
|
+
isFlask = true;
|
|
389
|
+
if (content.includes('django'))
|
|
390
|
+
isDjango = true;
|
|
391
|
+
};
|
|
392
|
+
if (fs.existsSync(reqPath))
|
|
393
|
+
checkPyManifest(fs.readFileSync(reqPath, 'utf8'));
|
|
394
|
+
if (fs.existsSync(pyprojectPath))
|
|
395
|
+
checkPyManifest(fs.readFileSync(pyprojectPath, 'utf8'));
|
|
396
|
+
if (isFastApi)
|
|
397
|
+
framework = 'fastapi';
|
|
398
|
+
else if (isFlask)
|
|
399
|
+
framework = 'flask';
|
|
400
|
+
else if (isDjango)
|
|
401
|
+
framework = 'django';
|
|
402
|
+
else
|
|
403
|
+
framework = 'generic-python';
|
|
404
|
+
const commonPythonEntries = [
|
|
405
|
+
'main.py', 'app.py',
|
|
406
|
+
'src/main.py', 'src/app.py',
|
|
407
|
+
'server.py', 'src/server.py',
|
|
408
|
+
'wsgi.py', 'asgi.py',
|
|
409
|
+
];
|
|
410
|
+
for (const entry of commonPythonEntries) {
|
|
411
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
412
|
+
return { framework, entryFile: entry, warnings };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// 3. Go detection
|
|
417
|
+
const goModPath = path.join(projectDir, 'go.mod');
|
|
418
|
+
if (fs.existsSync(goModPath)) {
|
|
419
|
+
try {
|
|
420
|
+
const goMod = fs.readFileSync(goModPath, 'utf8');
|
|
421
|
+
if (goMod.includes('gin-gonic/gin')) {
|
|
422
|
+
framework = 'go-gin';
|
|
423
|
+
}
|
|
424
|
+
else if (goMod.includes('go-chi/chi')) {
|
|
425
|
+
framework = 'go-chi';
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
framework = 'generic-go';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
framework = 'generic-go';
|
|
433
|
+
}
|
|
434
|
+
const commonGoEntries = [
|
|
435
|
+
'main.go', 'server.go',
|
|
436
|
+
'cmd/server/main.go', 'cmd/main.go',
|
|
437
|
+
'src/main.go',
|
|
438
|
+
];
|
|
439
|
+
for (const entry of commonGoEntries) {
|
|
440
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
441
|
+
return { framework, entryFile: entry, warnings };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// 4. Rust detection
|
|
446
|
+
const cargoPath = path.join(projectDir, 'Cargo.toml');
|
|
447
|
+
if (fs.existsSync(cargoPath)) {
|
|
448
|
+
try {
|
|
449
|
+
const cargo = fs.readFileSync(cargoPath, 'utf8');
|
|
450
|
+
if (cargo.includes('actix-web'))
|
|
451
|
+
framework = 'rust-actix';
|
|
452
|
+
else if (cargo.includes('axum'))
|
|
453
|
+
framework = 'rust-axum';
|
|
454
|
+
else
|
|
455
|
+
framework = 'generic-rust';
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
framework = 'generic-rust';
|
|
459
|
+
}
|
|
460
|
+
const commonRustEntries = ['src/main.rs', 'main.rs', 'src/bin/server.rs'];
|
|
461
|
+
for (const entry of commonRustEntries) {
|
|
462
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
463
|
+
return { framework, entryFile: entry, warnings };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// 5. Java / Spring Boot detection
|
|
468
|
+
const pomPath = path.join(projectDir, 'pom.xml');
|
|
469
|
+
const gradlePath = path.join(projectDir, 'build.gradle');
|
|
470
|
+
if (fs.existsSync(pomPath) || fs.existsSync(gradlePath)) {
|
|
471
|
+
framework = 'java-spring';
|
|
472
|
+
const commonJavaEntries = [
|
|
473
|
+
'src/main/java/com/example/Application.java',
|
|
474
|
+
'src/main/java/Application.java',
|
|
475
|
+
'src/main/java/Main.java',
|
|
476
|
+
];
|
|
477
|
+
for (const entry of commonJavaEntries) {
|
|
478
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
479
|
+
return { framework, entryFile: entry, warnings };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// 6. Generic File Fallback (works even without package manifest)
|
|
484
|
+
const genericFallbacks = [
|
|
485
|
+
['main.go', 'generic-go'],
|
|
486
|
+
['server.go', 'generic-go'],
|
|
487
|
+
['src/main.rs', 'generic-rust'],
|
|
488
|
+
['main.py', 'generic-python'],
|
|
489
|
+
['app.py', 'generic-python'],
|
|
490
|
+
['server.js', 'generic-node'],
|
|
491
|
+
['index.js', 'generic-node'],
|
|
492
|
+
['server.ts', 'generic-node'],
|
|
493
|
+
['index.ts', 'generic-node'],
|
|
494
|
+
['Program.cs', 'csharp-dotnet'],
|
|
495
|
+
];
|
|
496
|
+
for (const [entry, fw] of genericFallbacks) {
|
|
497
|
+
if (fs.existsSync(path.join(projectDir, entry))) {
|
|
498
|
+
return { framework: fw, entryFile: entry, warnings };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { framework, warnings };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FaultMesh Unified Diff Generator
|
|
3
|
+
* Produces standard Git-compatible unified diffs for reviewable code remediation.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DiffGenerator {
|
|
6
|
+
/**
|
|
7
|
+
* Generates a unified diff string comparing original and remediated content
|
|
8
|
+
*/
|
|
9
|
+
static generateDiff(filePath: string, original: string, remediated: string): string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FaultMesh Unified Diff Generator
|
|
3
|
+
* Produces standard Git-compatible unified diffs for reviewable code remediation.
|
|
4
|
+
*/
|
|
5
|
+
export class DiffGenerator {
|
|
6
|
+
/**
|
|
7
|
+
* Generates a unified diff string comparing original and remediated content
|
|
8
|
+
*/
|
|
9
|
+
static generateDiff(filePath, original, remediated) {
|
|
10
|
+
const origLines = original.split(/\r?\n/);
|
|
11
|
+
const remLines = remediated.split(/\r?\n/);
|
|
12
|
+
const normPath = filePath.replace(/\\/g, '/');
|
|
13
|
+
const header = [
|
|
14
|
+
`--- a/${normPath}`,
|
|
15
|
+
`+++ b/${normPath}`,
|
|
16
|
+
];
|
|
17
|
+
if (original === remediated) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
// Find the first line that differs
|
|
21
|
+
let startLine = 0;
|
|
22
|
+
while (startLine < origLines.length &&
|
|
23
|
+
startLine < remLines.length &&
|
|
24
|
+
origLines[startLine] === remLines[startLine]) {
|
|
25
|
+
startLine++;
|
|
26
|
+
}
|
|
27
|
+
// Find common trailing lines
|
|
28
|
+
let origEnd = origLines.length - 1;
|
|
29
|
+
let remEnd = remLines.length - 1;
|
|
30
|
+
while (origEnd > startLine &&
|
|
31
|
+
remEnd > startLine &&
|
|
32
|
+
origLines[origEnd] === remLines[remEnd]) {
|
|
33
|
+
origEnd--;
|
|
34
|
+
remEnd--;
|
|
35
|
+
}
|
|
36
|
+
// Context lines before difference (up to 3)
|
|
37
|
+
const contextBefore = Math.max(0, startLine - 3);
|
|
38
|
+
const hunkOrigStart = contextBefore + 1;
|
|
39
|
+
const hunkRemStart = contextBefore + 1;
|
|
40
|
+
const hunkLines = [];
|
|
41
|
+
// Add context before
|
|
42
|
+
for (let i = contextBefore; i < startLine; i++) {
|
|
43
|
+
hunkLines.push(` ${origLines[i]}`);
|
|
44
|
+
}
|
|
45
|
+
// Add removed lines
|
|
46
|
+
for (let i = startLine; i <= origEnd; i++) {
|
|
47
|
+
hunkLines.push(`-${origLines[i]}`);
|
|
48
|
+
}
|
|
49
|
+
// Add added lines
|
|
50
|
+
for (let i = startLine; i <= remEnd; i++) {
|
|
51
|
+
hunkLines.push(`+${remLines[i]}`);
|
|
52
|
+
}
|
|
53
|
+
// Context lines after difference (up to 3)
|
|
54
|
+
const contextAfter = Math.min(origLines.length, origEnd + 4);
|
|
55
|
+
for (let i = origEnd + 1; i < contextAfter; i++) {
|
|
56
|
+
hunkLines.push(` ${origLines[i]}`);
|
|
57
|
+
}
|
|
58
|
+
const origCount = hunkLines.filter(l => l.startsWith(' ') || l.startsWith('-')).length;
|
|
59
|
+
const remCount = hunkLines.filter(l => l.startsWith(' ') || l.startsWith('+')).length;
|
|
60
|
+
const rangeHeader = `@@ -${hunkOrigStart},${origCount} +${hunkRemStart},${remCount} @@`;
|
|
61
|
+
return [...header, rangeHeader, ...hunkLines].join('\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class DiffUtil {
|
|
2
|
+
/**
|
|
3
|
+
* Generates a standard unified git diff between original and modified text
|
|
4
|
+
*/
|
|
5
|
+
static generateUnifiedDiff(filePath, originalText, newText) {
|
|
6
|
+
const originalLines = originalText.split(/\r?\n/);
|
|
7
|
+
const newLines = newText.split(/\r?\n/);
|
|
8
|
+
if (originalText === newText) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
const diffLines = [
|
|
12
|
+
`--- a/${filePath}`,
|
|
13
|
+
`+++ b/${filePath}`,
|
|
14
|
+
];
|
|
15
|
+
// Simple line-by-line diffing algorithm for targeted patch generation
|
|
16
|
+
let origIdx = 0;
|
|
17
|
+
let newIdx = 0;
|
|
18
|
+
while (origIdx < originalLines.length || newIdx < newLines.length) {
|
|
19
|
+
if (origIdx < originalLines.length && newIdx < newLines.length && originalLines[origIdx] === newLines[newIdx]) {
|
|
20
|
+
origIdx++;
|
|
21
|
+
newIdx++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Found a difference: compute hunk
|
|
25
|
+
const startOrig = Math.max(0, origIdx - 2);
|
|
26
|
+
const startNew = Math.max(0, newIdx - 2);
|
|
27
|
+
// Find extent of change
|
|
28
|
+
let nextMatchOrig = origIdx;
|
|
29
|
+
let nextMatchNew = newIdx;
|
|
30
|
+
let matched = false;
|
|
31
|
+
for (let oi = origIdx; oi < Math.min(originalLines.length, origIdx + 20); oi++) {
|
|
32
|
+
for (let ni = newIdx; ni < Math.min(newLines.length, newIdx + 20); ni++) {
|
|
33
|
+
if (originalLines[oi] === newLines[ni] && originalLines[oi].trim().length > 0) {
|
|
34
|
+
nextMatchOrig = oi;
|
|
35
|
+
nextMatchNew = ni;
|
|
36
|
+
matched = true;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (matched)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
if (!matched) {
|
|
44
|
+
nextMatchOrig = originalLines.length;
|
|
45
|
+
nextMatchNew = newLines.length;
|
|
46
|
+
}
|
|
47
|
+
const origCount = nextMatchOrig - startOrig;
|
|
48
|
+
const newCount = nextMatchNew - startNew;
|
|
49
|
+
diffLines.push(`@@ -${startOrig + 1},${origCount} +${startNew + 1},${newCount} @@`);
|
|
50
|
+
// Leading context
|
|
51
|
+
for (let i = startOrig; i < origIdx; i++) {
|
|
52
|
+
diffLines.push(` ${originalLines[i]}`);
|
|
53
|
+
}
|
|
54
|
+
// Deletions
|
|
55
|
+
for (let i = origIdx; i < nextMatchOrig; i++) {
|
|
56
|
+
diffLines.push(`-${originalLines[i]}`);
|
|
57
|
+
}
|
|
58
|
+
// Additions
|
|
59
|
+
for (let i = newIdx; i < nextMatchNew; i++) {
|
|
60
|
+
diffLines.push(`+${newLines[i]}`);
|
|
61
|
+
}
|
|
62
|
+
origIdx = nextMatchOrig;
|
|
63
|
+
newIdx = nextMatchNew;
|
|
64
|
+
}
|
|
65
|
+
return diffLines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
}
|