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.
Files changed (43) hide show
  1. package/README.md +61 -19
  2. package/dist/dashboard/app.js +1906 -139
  3. package/dist/dashboard/index.html +393 -99
  4. package/dist/dashboard/styles.css +1191 -23
  5. package/dist/engine/ControlApi.d.ts +13 -1
  6. package/dist/engine/ControlApi.js +304 -14
  7. package/dist/engine/FaultMeshProxy.d.ts +2 -0
  8. package/dist/engine/FaultMeshProxy.js +41 -5
  9. package/dist/engine/TelemetryHub.d.ts +1 -0
  10. package/dist/engine/TelemetryHub.js +3 -0
  11. package/dist/engine/ToxicPipeline.d.ts +5 -4
  12. package/dist/engine/ToxicPipeline.js +17 -4
  13. package/dist/healer/AiHealer.d.ts +30 -0
  14. package/dist/healer/AiHealer.js +188 -0
  15. package/dist/healer/AutoHealer.d.ts +24 -0
  16. package/dist/healer/AutoHealer.js +503 -0
  17. package/dist/healer/DiffGenerator.d.ts +10 -0
  18. package/dist/healer/DiffGenerator.js +63 -0
  19. package/dist/healer/DiffUtil.d.ts +6 -0
  20. package/dist/healer/DiffUtil.js +67 -0
  21. package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
  22. package/dist/healer/transformers/ExpressTransformers.js +228 -0
  23. package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
  24. package/dist/healer/transformers/FastApiTransformers.js +93 -0
  25. package/dist/healer/transformers/GoTransformers.d.ts +19 -0
  26. package/dist/healer/transformers/GoTransformers.js +106 -0
  27. package/dist/healer/types.d.ts +59 -0
  28. package/dist/healer/types.js +1 -0
  29. package/dist/redteam/EccAgentBridge.d.ts +20 -0
  30. package/dist/redteam/EccAgentBridge.js +303 -0
  31. package/dist/redteam/RedTeamEngine.d.ts +56 -0
  32. package/dist/redteam/RedTeamEngine.js +709 -0
  33. package/dist/redteam/types.d.ts +117 -0
  34. package/dist/redteam/types.js +5 -0
  35. package/dist/scorer/ResilienceScorer.d.ts +5 -0
  36. package/dist/scorer/ResilienceScorer.js +323 -7
  37. package/dist/scorer/SecurityAuditor.d.ts +8 -0
  38. package/dist/scorer/SecurityAuditor.js +471 -69
  39. package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
  40. package/dist/scorer/TrafficStormAuditor.js +328 -69
  41. package/dist/server.js +17 -10
  42. package/dist/types.d.ts +7 -3
  43. 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,6 @@
1
+ export declare class DiffUtil {
2
+ /**
3
+ * Generates a standard unified git diff between original and modified text
4
+ */
5
+ static generateUnifiedDiff(filePath: string, originalText: string, newText: string): string;
6
+ }
@@ -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
+ }