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
|
@@ -3,6 +3,9 @@ import { TelemetryHub } from './TelemetryHub.js';
|
|
|
3
3
|
import { ResilienceScorer } from '../scorer/ResilienceScorer.js';
|
|
4
4
|
import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
|
|
5
5
|
import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
|
|
6
|
+
import { FaultMeshProxy } from './FaultMeshProxy.js';
|
|
7
|
+
import { EccAgentBridge } from '../redteam/EccAgentBridge.js';
|
|
8
|
+
import { RedTeamEngine } from '../redteam/RedTeamEngine.js';
|
|
6
9
|
export declare class ControlApi {
|
|
7
10
|
private server;
|
|
8
11
|
private port;
|
|
@@ -12,12 +15,21 @@ export declare class ControlApi {
|
|
|
12
15
|
private securityAuditor;
|
|
13
16
|
private trafficStormAuditor;
|
|
14
17
|
private dashboardDir;
|
|
18
|
+
private proxy?;
|
|
19
|
+
private eccBridge;
|
|
20
|
+
private redTeamEngine;
|
|
15
21
|
private isRunning;
|
|
16
|
-
constructor(port: number, pipeline: ToxicPipeline, telemetryHub: TelemetryHub, scorer: ResilienceScorer, securityAuditor?: SecurityAuditor, trafficStormAuditor?: TrafficStormAuditor, dashboardDir?: string);
|
|
22
|
+
constructor(port: number, pipeline: ToxicPipeline, telemetryHub: TelemetryHub, scorer: ResilienceScorer, securityAuditor?: SecurityAuditor, trafficStormAuditor?: TrafficStormAuditor, dashboardDir?: string, proxy?: FaultMeshProxy, redTeamEngine?: RedTeamEngine, eccBridge?: EccAgentBridge);
|
|
17
23
|
private handleRequest;
|
|
24
|
+
setProxy(proxy: FaultMeshProxy): void;
|
|
25
|
+
getRedTeamEngine(): RedTeamEngine;
|
|
26
|
+
getEccBridge(): EccAgentBridge;
|
|
18
27
|
private serveStatic;
|
|
19
28
|
private readBody;
|
|
20
29
|
private json;
|
|
30
|
+
private sampleProcess?;
|
|
31
|
+
private restartSampleBackend;
|
|
32
|
+
private killPort;
|
|
21
33
|
start(): Promise<void>;
|
|
22
34
|
stop(): Promise<void>;
|
|
23
35
|
getPort(): number;
|
|
@@ -2,8 +2,12 @@ import http from 'node:http';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execSync, spawn } from 'node:child_process';
|
|
5
6
|
import { SecurityAuditor } from '../scorer/SecurityAuditor.js';
|
|
6
7
|
import { TrafficStormAuditor } from '../scorer/TrafficStormAuditor.js';
|
|
8
|
+
import { AutoHealer } from '../healer/AutoHealer.js';
|
|
9
|
+
import { EccAgentBridge } from '../redteam/EccAgentBridge.js';
|
|
10
|
+
import { RedTeamEngine } from '../redteam/RedTeamEngine.js';
|
|
7
11
|
export class ControlApi {
|
|
8
12
|
server;
|
|
9
13
|
port = 0;
|
|
@@ -13,14 +17,20 @@ export class ControlApi {
|
|
|
13
17
|
securityAuditor;
|
|
14
18
|
trafficStormAuditor;
|
|
15
19
|
dashboardDir;
|
|
20
|
+
proxy;
|
|
21
|
+
eccBridge;
|
|
22
|
+
redTeamEngine;
|
|
16
23
|
isRunning = false;
|
|
17
|
-
constructor(port, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, dashboardDir) {
|
|
24
|
+
constructor(port, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, dashboardDir, proxy, redTeamEngine, eccBridge) {
|
|
18
25
|
this.port = port;
|
|
19
26
|
this.pipeline = pipeline;
|
|
20
27
|
this.telemetryHub = telemetryHub;
|
|
21
28
|
this.scorer = scorer;
|
|
22
29
|
this.securityAuditor = securityAuditor || new SecurityAuditor('http://127.0.0.1:3001');
|
|
23
30
|
this.trafficStormAuditor = trafficStormAuditor || new TrafficStormAuditor('http://127.0.0.1:3001');
|
|
31
|
+
this.proxy = proxy;
|
|
32
|
+
this.eccBridge = eccBridge || new EccAgentBridge();
|
|
33
|
+
this.redTeamEngine = redTeamEngine || new RedTeamEngine(this.pipeline, this.telemetryHub, this.eccBridge);
|
|
24
34
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
35
|
let resolvedDir = dashboardDir || path.resolve(currentDir, '../dashboard');
|
|
26
36
|
if (!fs.existsSync(resolvedDir)) {
|
|
@@ -33,6 +43,11 @@ export class ControlApi {
|
|
|
33
43
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
34
44
|
}
|
|
35
45
|
async handleRequest(req, res) {
|
|
46
|
+
const rawUrl = req.url || '/';
|
|
47
|
+
if (rawUrl.includes('/..') || rawUrl.includes('..\\') || rawUrl.toLowerCase().includes('%2e%2e') || rawUrl.includes('\0')) {
|
|
48
|
+
this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
36
51
|
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
37
52
|
const pathname = url.pathname;
|
|
38
53
|
// Enable CORS for local dashboards or external runners
|
|
@@ -189,6 +204,186 @@ export class ControlApi {
|
|
|
189
204
|
}
|
|
190
205
|
return;
|
|
191
206
|
}
|
|
207
|
+
// 8.5 Dynamic Target API Configuration
|
|
208
|
+
if (pathname === '/_faultmesh/config/target') {
|
|
209
|
+
if (req.method === 'GET') {
|
|
210
|
+
const currentTarget = this.proxy ? this.proxy.getTargetUrl() : 'http://127.0.0.1:4000';
|
|
211
|
+
this.json(res, 200, { targetUrl: currentTarget });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (req.method === 'POST') {
|
|
215
|
+
const raw = await this.readBody(req);
|
|
216
|
+
try {
|
|
217
|
+
const body = JSON.parse(raw);
|
|
218
|
+
if (!body.targetUrl || typeof body.targetUrl !== 'string') {
|
|
219
|
+
this.json(res, 400, { error: 'targetUrl is required and must be a string' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Validate URL format
|
|
223
|
+
new URL(body.targetUrl);
|
|
224
|
+
if (this.proxy) {
|
|
225
|
+
this.proxy.setTargetUrl(body.targetUrl);
|
|
226
|
+
}
|
|
227
|
+
if (this.securityAuditor) {
|
|
228
|
+
this.securityAuditor.setTargetUrl(body.targetUrl);
|
|
229
|
+
}
|
|
230
|
+
if (this.trafficStormAuditor) {
|
|
231
|
+
this.trafficStormAuditor.setTargetUrl(body.targetUrl);
|
|
232
|
+
}
|
|
233
|
+
this.json(res, 200, { success: true, targetUrl: body.targetUrl });
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
this.json(res, 400, { error: 'Invalid target URL format', details: err.message });
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// 8.6 Auto-Healer Endpoints
|
|
242
|
+
if (pathname === '/_faultmesh/healer/scan' && req.method === 'POST') {
|
|
243
|
+
try {
|
|
244
|
+
const raw = await this.readBody(req);
|
|
245
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
246
|
+
const projectDir = body.projectDir || '.';
|
|
247
|
+
const failedChecks = body.failedChecks || [];
|
|
248
|
+
const aiConfig = body.aiConfig;
|
|
249
|
+
const engineMode = body.engineMode;
|
|
250
|
+
const scanResult = await AutoHealer.scan({ projectDir, failedChecks, aiConfig, engineMode });
|
|
251
|
+
this.json(res, 200, scanResult);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
this.json(res, 500, { error: 'AutoHealer scan error', details: err.message });
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (pathname === '/_faultmesh/healer/apply' && req.method === 'POST') {
|
|
259
|
+
try {
|
|
260
|
+
const raw = await this.readBody(req);
|
|
261
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
262
|
+
const projectDir = body.projectDir || '.';
|
|
263
|
+
const patchIds = body.patchIds;
|
|
264
|
+
const failedChecks = body.failedChecks;
|
|
265
|
+
const createBackup = body.createBackup !== false;
|
|
266
|
+
const aiConfig = body.aiConfig;
|
|
267
|
+
const engineMode = body.engineMode;
|
|
268
|
+
const applyResult = await AutoHealer.apply({ projectDir, patchIds, failedChecks, createBackup, aiConfig, engineMode });
|
|
269
|
+
// If healing the sample backend, automatically restart it so memory updates immediately and switch proxy target
|
|
270
|
+
if (applyResult.success && projectDir.includes('vulnerable-backend')) {
|
|
271
|
+
await this.restartSampleBackend();
|
|
272
|
+
if (this.proxy) {
|
|
273
|
+
this.proxy.setTargetUrl('http://127.0.0.1:5050');
|
|
274
|
+
}
|
|
275
|
+
if (this.securityAuditor) {
|
|
276
|
+
this.securityAuditor.setTargetUrl('http://127.0.0.1:5050');
|
|
277
|
+
}
|
|
278
|
+
if (this.trafficStormAuditor) {
|
|
279
|
+
this.trafficStormAuditor.setTargetUrl('http://127.0.0.1:5050');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.json(res, 200, applyResult);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.json(res, 500, { error: 'AutoHealer apply error', details: err.message });
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (pathname === '/_faultmesh/healer/rollback' && req.method === 'POST') {
|
|
290
|
+
try {
|
|
291
|
+
const raw = await this.readBody(req);
|
|
292
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
293
|
+
if (!body.backupDir) {
|
|
294
|
+
this.json(res, 400, { error: 'backupDir is required for rollback' });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const projectDir = body.projectDir || '.';
|
|
298
|
+
const rollbackResult = await AutoHealer.rollback({ projectDir, backupDir: body.backupDir });
|
|
299
|
+
if (rollbackResult.success && projectDir.includes('vulnerable-backend')) {
|
|
300
|
+
await this.restartSampleBackend();
|
|
301
|
+
}
|
|
302
|
+
this.json(res, 200, rollbackResult);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
this.json(res, 500, { error: 'AutoHealer rollback error', details: err.message });
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (pathname === '/_faultmesh/sample/restart' && req.method === 'POST') {
|
|
310
|
+
try {
|
|
311
|
+
await this.restartSampleBackend();
|
|
312
|
+
this.json(res, 200, { success: true, message: 'Sample backend restarted on port 5050' });
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
this.json(res, 500, { error: 'Failed to restart sample backend', details: err.message });
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (pathname === '/_faultmesh/sample/reset' && req.method === 'POST') {
|
|
320
|
+
try {
|
|
321
|
+
const baselinePath = path.resolve(process.cwd(), 'examples/vulnerable-backend/baseline-vulnerable.js');
|
|
322
|
+
const targetPath = path.resolve(process.cwd(), 'examples/vulnerable-backend/server.js');
|
|
323
|
+
if (fs.existsSync(baselinePath)) {
|
|
324
|
+
fs.copyFileSync(baselinePath, targetPath);
|
|
325
|
+
}
|
|
326
|
+
await this.restartSampleBackend();
|
|
327
|
+
if (this.proxy) {
|
|
328
|
+
this.proxy.setTargetUrl('http://127.0.0.1:5050');
|
|
329
|
+
}
|
|
330
|
+
this.json(res, 200, { success: true, message: 'Sample backend restored to baseline vulnerable state on port 5050' });
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
this.json(res, 500, { error: 'Failed to reset sample backend', details: err.message });
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// 8.8 Autonomous Red Chaos Team Engine
|
|
338
|
+
if (pathname === '/_faultmesh/redteam/personas' && req.method === 'GET') {
|
|
339
|
+
try {
|
|
340
|
+
const personas = await this.eccBridge.getPersonas();
|
|
341
|
+
this.json(res, 200, { success: true, personas });
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
this.json(res, 500, { error: 'Failed to load agent personas', details: err.message });
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (pathname === '/_faultmesh/redteam/start' && req.method === 'POST') {
|
|
349
|
+
try {
|
|
350
|
+
const raw = await this.readBody(req);
|
|
351
|
+
const options = raw ? JSON.parse(raw) : {};
|
|
352
|
+
if (options.targetUrl && options.targetUrl.includes(':5050')) {
|
|
353
|
+
if (this.proxy) {
|
|
354
|
+
this.proxy.setTargetUrl('http://127.0.0.1:5050');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!options.targetUrl || options.targetUrl.includes(':4000') || options.targetUrl.includes(':5050')) {
|
|
358
|
+
options.targetUrl = 'http://127.0.0.1:3001';
|
|
359
|
+
}
|
|
360
|
+
// Start campaign asynchronously in background
|
|
361
|
+
const campaignPromise = this.redTeamEngine.startCampaign(options);
|
|
362
|
+
campaignPromise.catch(err => {
|
|
363
|
+
console.error('Red Team background campaign error:', err.message);
|
|
364
|
+
});
|
|
365
|
+
const initialStatus = this.redTeamEngine.getStatus();
|
|
366
|
+
this.json(res, 202, { success: true, message: 'Red Team campaign initiated', status: initialStatus });
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
this.json(res, 400, { error: 'Failed to start Red Team campaign', details: err.message });
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (pathname === '/_faultmesh/redteam/abort' && req.method === 'POST') {
|
|
374
|
+
const result = this.redTeamEngine.abortCampaign();
|
|
375
|
+
this.json(res, 200, result);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (pathname === '/_faultmesh/redteam/status' && req.method === 'GET') {
|
|
379
|
+
this.json(res, 200, this.redTeamEngine.getStatus());
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (pathname === '/_faultmesh/redteam/report' && req.method === 'GET') {
|
|
383
|
+
const report = this.redTeamEngine.getReport();
|
|
384
|
+
this.json(res, 200, report || { message: 'No campaign report recorded yet' });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
192
387
|
// 9. Static Dashboard Serving
|
|
193
388
|
if (req.method === 'GET') {
|
|
194
389
|
this.serveStatic(pathname, res);
|
|
@@ -196,14 +391,40 @@ export class ControlApi {
|
|
|
196
391
|
}
|
|
197
392
|
this.json(res, 404, { error: 'Route not found' });
|
|
198
393
|
}
|
|
394
|
+
setProxy(proxy) {
|
|
395
|
+
this.proxy = proxy;
|
|
396
|
+
}
|
|
397
|
+
getRedTeamEngine() {
|
|
398
|
+
return this.redTeamEngine;
|
|
399
|
+
}
|
|
400
|
+
getEccBridge() {
|
|
401
|
+
return this.eccBridge;
|
|
402
|
+
}
|
|
199
403
|
serveStatic(pathname, res) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
404
|
+
const normalizedDashboardDir = path.resolve(this.dashboardDir);
|
|
405
|
+
let decodedPath = '';
|
|
406
|
+
try {
|
|
407
|
+
decodedPath = decodeURIComponent(pathname);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
this.json(res, 400, { error: 'Invalid URL encoding' });
|
|
205
411
|
return;
|
|
206
412
|
}
|
|
413
|
+
// Explicit Traversal Guard: reject relative parent indicators or backslashes
|
|
414
|
+
if (decodedPath.includes('..') || decodedPath.includes('\\')) {
|
|
415
|
+
this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const safePath = decodedPath === '/' ? 'index.html' : decodedPath.replace(/^\/+/, '');
|
|
419
|
+
const resolvedPath = path.resolve(normalizedDashboardDir, safePath);
|
|
420
|
+
// Strict Path Jailing: prevent directory traversal outside dashboardDir
|
|
421
|
+
const isWithinDashboard = resolvedPath.startsWith(normalizedDashboardDir + path.sep) ||
|
|
422
|
+
resolvedPath === path.join(normalizedDashboardDir, 'index.html');
|
|
423
|
+
if (!isWithinDashboard) {
|
|
424
|
+
this.json(res, 403, { error: 'Access Denied: Path Traversal Prohibited' });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const filePath = resolvedPath;
|
|
207
428
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
208
429
|
const ext = path.extname(filePath);
|
|
209
430
|
const contentTypes = {
|
|
@@ -217,15 +438,17 @@ export class ControlApi {
|
|
|
217
438
|
fs.createReadStream(filePath).pipe(res);
|
|
218
439
|
}
|
|
219
440
|
else {
|
|
220
|
-
// Fallback to index.html for SPA if
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
fs.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
441
|
+
// Fallback to index.html for SPA only if requesting an extensionless path (e.g. client routes)
|
|
442
|
+
const ext = path.extname(filePath);
|
|
443
|
+
if (!ext) {
|
|
444
|
+
const indexPath = path.join(this.dashboardDir, 'index.html');
|
|
445
|
+
if (fs.existsSync(indexPath)) {
|
|
446
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
447
|
+
fs.createReadStream(indexPath).pipe(res);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
228
450
|
}
|
|
451
|
+
this.json(res, 404, { error: 'File not found' });
|
|
229
452
|
}
|
|
230
453
|
}
|
|
231
454
|
readBody(req) {
|
|
@@ -240,6 +463,65 @@ export class ControlApi {
|
|
|
240
463
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
241
464
|
res.end(JSON.stringify(data));
|
|
242
465
|
}
|
|
466
|
+
sampleProcess;
|
|
467
|
+
async restartSampleBackend() {
|
|
468
|
+
if (this.sampleProcess) {
|
|
469
|
+
try {
|
|
470
|
+
this.sampleProcess.kill();
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
this.sampleProcess = undefined;
|
|
474
|
+
}
|
|
475
|
+
this.killPort(5050);
|
|
476
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
477
|
+
const sampleScript = path.resolve(process.cwd(), 'examples/vulnerable-backend/server.js');
|
|
478
|
+
if (!fs.existsSync(sampleScript)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const child = spawn(process.execPath, [sampleScript], {
|
|
482
|
+
cwd: path.dirname(sampleScript),
|
|
483
|
+
stdio: 'ignore',
|
|
484
|
+
detached: false,
|
|
485
|
+
});
|
|
486
|
+
this.sampleProcess = child;
|
|
487
|
+
const deadline = Date.now() + 6000;
|
|
488
|
+
while (Date.now() < deadline) {
|
|
489
|
+
try {
|
|
490
|
+
const res = await fetch('http://127.0.0.1:5050/api/health', {
|
|
491
|
+
signal: AbortSignal.timeout(500),
|
|
492
|
+
});
|
|
493
|
+
if (res.ok) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch { }
|
|
498
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
killPort(port) {
|
|
502
|
+
try {
|
|
503
|
+
if (process.platform === 'win32') {
|
|
504
|
+
const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' }).trim();
|
|
505
|
+
if (output) {
|
|
506
|
+
const lines = output.split('\n');
|
|
507
|
+
for (const line of lines) {
|
|
508
|
+
const parts = line.trim().split(/\s+/);
|
|
509
|
+
const pid = parts[parts.length - 1];
|
|
510
|
+
if (pid && pid !== '0' && pid !== String(process.pid)) {
|
|
511
|
+
try {
|
|
512
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
513
|
+
}
|
|
514
|
+
catch { }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
execSync(`fuser -k ${port}/tcp 2>/dev/null || true`, { stdio: 'ignore' });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch { }
|
|
524
|
+
}
|
|
243
525
|
start() {
|
|
244
526
|
return new Promise((resolve, reject) => {
|
|
245
527
|
this.server.listen(this.port, () => {
|
|
@@ -254,6 +536,14 @@ export class ControlApi {
|
|
|
254
536
|
});
|
|
255
537
|
}
|
|
256
538
|
stop() {
|
|
539
|
+
if (this.sampleProcess) {
|
|
540
|
+
try {
|
|
541
|
+
this.sampleProcess.kill();
|
|
542
|
+
}
|
|
543
|
+
catch { }
|
|
544
|
+
this.sampleProcess = undefined;
|
|
545
|
+
}
|
|
546
|
+
this.killPort(5050);
|
|
257
547
|
return new Promise((resolve) => {
|
|
258
548
|
if (!this.isRunning)
|
|
259
549
|
return resolve();
|
|
@@ -28,8 +28,9 @@ export class FaultMeshProxy {
|
|
|
28
28
|
res.end();
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
+
const requestPath = req.url || '/';
|
|
31
32
|
// 1. Check for immediate downstream Status override toxic
|
|
32
|
-
const statusToxic = this.toxicPipeline.getActiveStatusToxic('downstream');
|
|
33
|
+
const statusToxic = this.toxicPipeline.getActiveStatusToxic('downstream', requestPath);
|
|
33
34
|
if (statusToxic) {
|
|
34
35
|
appliedToxics.push(`Status (${statusToxic.statusCode})`);
|
|
35
36
|
const body = statusToxic.responseBody || JSON.stringify({
|
|
@@ -37,11 +38,32 @@ export class FaultMeshProxy {
|
|
|
37
38
|
code: statusToxic.statusCode,
|
|
38
39
|
faultMesh: true,
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
+
// Check if there is also an active downstream latency toxic
|
|
42
|
+
const latencyRule = this.toxicPipeline.getRules().find(r => r.enabled &&
|
|
43
|
+
r.type === 'latency' &&
|
|
44
|
+
r.direction === 'downstream' &&
|
|
45
|
+
(!r.pathPattern || !r.pathPattern.trim() || requestPath.includes(r.pathPattern.trim())));
|
|
46
|
+
if (latencyRule && latencyRule.config) {
|
|
47
|
+
const lCfg = latencyRule.config;
|
|
48
|
+
let delay = Math.max(0, lCfg.latencyMs || 0);
|
|
49
|
+
if (lCfg.jitterMs) {
|
|
50
|
+
const jitter = (Math.random() * 2 - 1) * lCfg.jitterMs;
|
|
51
|
+
delay = Math.max(0, Math.round(delay + jitter));
|
|
52
|
+
}
|
|
53
|
+
if (delay > 0) {
|
|
54
|
+
appliedToxics.push(`Latency (${delay}ms)`);
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const headers = {
|
|
41
59
|
'Content-Type': 'application/json',
|
|
42
60
|
'X-FaultMesh-Injected': 'status',
|
|
43
61
|
'Access-Control-Allow-Origin': '*',
|
|
44
|
-
}
|
|
62
|
+
};
|
|
63
|
+
if (statusToxic.statusCode === 429) {
|
|
64
|
+
headers['Retry-After'] = '15';
|
|
65
|
+
}
|
|
66
|
+
res.writeHead(statusToxic.statusCode, headers);
|
|
45
67
|
res.end(body);
|
|
46
68
|
this.telemetryHub.recordRequestComplete({
|
|
47
69
|
id: requestId,
|
|
@@ -58,17 +80,25 @@ export class FaultMeshProxy {
|
|
|
58
80
|
}
|
|
59
81
|
// 2. Prepare Upstream forwarding
|
|
60
82
|
const targetUrl = new URL(req.url || '/', this.config.targetUrl);
|
|
83
|
+
const clientHost = req.headers['host'];
|
|
84
|
+
const forwardedHost = clientHost && !clientHost.includes('3001')
|
|
85
|
+
? clientHost
|
|
86
|
+
: targetUrl.host;
|
|
61
87
|
const options = {
|
|
62
88
|
protocol: targetUrl.protocol,
|
|
63
89
|
hostname: targetUrl.hostname,
|
|
64
90
|
port: targetUrl.port,
|
|
65
91
|
path: targetUrl.pathname + targetUrl.search,
|
|
66
92
|
method: req.method,
|
|
67
|
-
headers: {
|
|
93
|
+
headers: {
|
|
94
|
+
...req.headers,
|
|
95
|
+
host: forwardedHost,
|
|
96
|
+
'x-forwarded-host': clientHost || targetUrl.host,
|
|
97
|
+
},
|
|
68
98
|
};
|
|
69
99
|
const upstreamReq = http.request(options, async (upstreamRes) => {
|
|
70
100
|
// 3. Prepare Downstream transformers
|
|
71
|
-
const { transformers: downstreamTransformers, appliedNames } = this.toxicPipeline.createStreamTransformers('downstream');
|
|
101
|
+
const { transformers: downstreamTransformers, appliedNames } = this.toxicPipeline.createStreamTransformers('downstream', requestPath);
|
|
72
102
|
appliedToxics.push(...appliedNames);
|
|
73
103
|
const headers = { ...upstreamRes.headers };
|
|
74
104
|
delete headers['content-length']; // Length changes if chunks are compressed, cut, or padded
|
|
@@ -164,4 +194,10 @@ export class FaultMeshProxy {
|
|
|
164
194
|
getPort() {
|
|
165
195
|
return this.port;
|
|
166
196
|
}
|
|
197
|
+
setTargetUrl(url) {
|
|
198
|
+
this.config.targetUrl = url;
|
|
199
|
+
}
|
|
200
|
+
getTargetUrl() {
|
|
201
|
+
return this.config.targetUrl;
|
|
202
|
+
}
|
|
167
203
|
}
|
|
@@ -11,6 +11,7 @@ export declare class TelemetryHub {
|
|
|
11
11
|
getMetrics(): ProxyMetrics;
|
|
12
12
|
getRecentEvents(): TelemetryEvent[];
|
|
13
13
|
registerSSEClient(res: ServerResponse): void;
|
|
14
|
+
broadcastCustomEvent(eventType: string, data: any): void;
|
|
14
15
|
private broadcastSSE;
|
|
15
16
|
clear(): void;
|
|
16
17
|
}
|
|
@@ -42,6 +42,9 @@ export class TelemetryHub {
|
|
|
42
42
|
this.sseClients.delete(res);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
broadcastCustomEvent(eventType, data) {
|
|
46
|
+
this.broadcastSSE(eventType, data);
|
|
47
|
+
}
|
|
45
48
|
broadcastSSE(eventType, data) {
|
|
46
49
|
const payload = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
47
50
|
for (const client of this.sseClients) {
|
|
@@ -8,14 +8,15 @@ export declare class ToxicPipeline {
|
|
|
8
8
|
getRules(): ToxicRule[];
|
|
9
9
|
getRule(id: string): ToxicRule | undefined;
|
|
10
10
|
clearRules(): void;
|
|
11
|
+
private matchesPath;
|
|
11
12
|
/**
|
|
12
|
-
* Check if any active StatusToxic exists for the specified direction
|
|
13
|
+
* Check if any active StatusToxic exists for the specified direction and request path
|
|
13
14
|
*/
|
|
14
|
-
getActiveStatusToxic(direction: ToxicStreamDirection): StatusToxic | null;
|
|
15
|
+
getActiveStatusToxic(direction: ToxicStreamDirection, requestPath?: string): StatusToxic | null;
|
|
15
16
|
/**
|
|
16
|
-
* Create an array of streaming transformers for the specified stream direction
|
|
17
|
+
* Create an array of streaming transformers for the specified stream direction and request path
|
|
17
18
|
*/
|
|
18
|
-
createStreamTransformers(direction: ToxicStreamDirection): {
|
|
19
|
+
createStreamTransformers(direction: ToxicStreamDirection, requestPath?: string): {
|
|
19
20
|
transformers: Transform[];
|
|
20
21
|
appliedNames: string[];
|
|
21
22
|
};
|
|
@@ -20,26 +20,39 @@ export class ToxicPipeline {
|
|
|
20
20
|
clearRules() {
|
|
21
21
|
this.rules.clear();
|
|
22
22
|
}
|
|
23
|
+
matchesPath(rule, requestPath) {
|
|
24
|
+
if (!rule.pathPattern || !rule.pathPattern.trim()) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (!requestPath) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return requestPath.includes(rule.pathPattern.trim());
|
|
31
|
+
}
|
|
23
32
|
/**
|
|
24
|
-
* Check if any active StatusToxic exists for the specified direction
|
|
33
|
+
* Check if any active StatusToxic exists for the specified direction and request path
|
|
25
34
|
*/
|
|
26
|
-
getActiveStatusToxic(direction) {
|
|
35
|
+
getActiveStatusToxic(direction, requestPath) {
|
|
27
36
|
for (const rule of this.rules.values()) {
|
|
28
37
|
if (rule.enabled && rule.type === 'status' && rule.direction === direction) {
|
|
38
|
+
if (!this.matchesPath(rule, requestPath))
|
|
39
|
+
continue;
|
|
29
40
|
return new StatusToxic(rule.config);
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
return null;
|
|
33
44
|
}
|
|
34
45
|
/**
|
|
35
|
-
* Create an array of streaming transformers for the specified stream direction
|
|
46
|
+
* Create an array of streaming transformers for the specified stream direction and request path
|
|
36
47
|
*/
|
|
37
|
-
createStreamTransformers(direction) {
|
|
48
|
+
createStreamTransformers(direction, requestPath) {
|
|
38
49
|
const transformers = [];
|
|
39
50
|
const appliedNames = [];
|
|
40
51
|
for (const rule of this.rules.values()) {
|
|
41
52
|
if (!rule.enabled || rule.direction !== direction)
|
|
42
53
|
continue;
|
|
54
|
+
if (!this.matchesPath(rule, requestPath))
|
|
55
|
+
continue;
|
|
43
56
|
switch (rule.type) {
|
|
44
57
|
case 'latency':
|
|
45
58
|
transformers.push(new LatencyToxic(rule.config));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AiProviderConfig, BackendFramework } from './types.js';
|
|
2
|
+
export interface AiRemediationResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
content: string;
|
|
5
|
+
description: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class AiHealer {
|
|
9
|
+
/**
|
|
10
|
+
* Checks if an AI provider is reachable or configured
|
|
11
|
+
*/
|
|
12
|
+
static isAvailable(config?: AiProviderConfig): Promise<{
|
|
13
|
+
available: boolean;
|
|
14
|
+
provider?: string;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Generates a surgical code remediation using the configured or detected LLM provider
|
|
18
|
+
*/
|
|
19
|
+
static generateRemediation(options: {
|
|
20
|
+
filePath: string;
|
|
21
|
+
originalContent: string;
|
|
22
|
+
framework: BackendFramework;
|
|
23
|
+
failedChecks: string[];
|
|
24
|
+
config?: AiProviderConfig;
|
|
25
|
+
}): Promise<AiRemediationResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Safely strips enclosing markdown code fences from model outputs
|
|
28
|
+
*/
|
|
29
|
+
private static extractCodeBlock;
|
|
30
|
+
}
|