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,709 @@
1
+ /**
2
+ * FaultMesh — Autonomous Red Chaos Team Engine
3
+ * RedTeamEngine: Coordinates adversarial multi-wave assault campaigns using autonomous agent personas.
4
+ */
5
+ import http from 'node:http';
6
+ import { EccAgentBridge } from './EccAgentBridge.js';
7
+ export class RedTeamEngine {
8
+ pipeline;
9
+ telemetryHub;
10
+ bridge;
11
+ currentPhase = 'idle';
12
+ activeCampaignId = null;
13
+ aborted = false;
14
+ active = false;
15
+ currentWaveIndex = 0;
16
+ totalWaves = 0;
17
+ currentWaveName = '';
18
+ activePersonaName = '';
19
+ probesSent = 0;
20
+ breaches = [];
21
+ logs = [];
22
+ startTime = 0;
23
+ endTime = 0;
24
+ targetUrl = 'http://127.0.0.1:3001';
25
+ intensity = 'sustained';
26
+ lastReport = null;
27
+ constructor(pipeline, telemetryHub, bridge) {
28
+ this.pipeline = pipeline;
29
+ this.telemetryHub = telemetryHub;
30
+ this.bridge = bridge || new EccAgentBridge();
31
+ }
32
+ getStatus() {
33
+ const critical = this.breaches.filter(b => b.severity === 'critical').length;
34
+ const high = this.breaches.filter(b => b.severity === 'high').length;
35
+ const medium = this.breaches.filter(b => b.severity === 'medium').length;
36
+ const low = this.breaches.filter(b => b.severity === 'low').length;
37
+ const progressPercent = this.totalWaves > 0
38
+ ? Math.min(100, Math.round(((this.currentWaveIndex + (this.active ? 0.5 : 0)) / this.totalWaves) * 100))
39
+ : 0;
40
+ return {
41
+ active: this.active,
42
+ campaignId: this.activeCampaignId,
43
+ phase: this.currentPhase,
44
+ progressPercent,
45
+ currentWave: this.currentWaveIndex,
46
+ totalWaves: this.totalWaves,
47
+ currentWaveName: this.currentWaveName,
48
+ activePersona: this.activePersonaName,
49
+ probesSent: this.probesSent,
50
+ breachesCount: {
51
+ critical,
52
+ high,
53
+ medium,
54
+ low,
55
+ total: this.breaches.length,
56
+ },
57
+ latestLog: this.logs.length > 0 ? this.logs[this.logs.length - 1] : undefined,
58
+ };
59
+ }
60
+ getReport() {
61
+ return this.lastReport;
62
+ }
63
+ async startCampaign(options = {}) {
64
+ if (this.active) {
65
+ throw new Error('A Red Team campaign is already in progress');
66
+ }
67
+ this.active = true;
68
+ this.aborted = false;
69
+ this.activeCampaignId = `camp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
70
+ this.targetUrl = options.targetUrl || 'http://127.0.0.1:3001';
71
+ this.intensity = options.intensity || 'sustained';
72
+ this.startTime = Date.now();
73
+ this.endTime = 0;
74
+ this.probesSent = 0;
75
+ this.breaches = [];
76
+ this.logs = [];
77
+ await this.bridge.initialize();
78
+ const personas = await this.bridge.getPersonas();
79
+ const waves = this.bridge.generateCampaignWaves(this.intensity);
80
+ this.totalWaves = waves.length;
81
+ this.addLog(0, 'RED-COMMAND', `Campaign initialized [${this.activeCampaignId}]. Target: ${this.targetUrl}. Intensity: ${this.intensity.toUpperCase()}`, 'info');
82
+ try {
83
+ // Phase 1: Recon & Target Fingerprinting
84
+ this.currentPhase = 'recon';
85
+ this.activePersonaName = 'Security Reviewer';
86
+ this.currentWaveName = 'Target Fingerprinting & Recon';
87
+ this.emitProgress();
88
+ await this.executeReconPhase();
89
+ if (this.aborted)
90
+ return this.finalizeAbortedCampaign();
91
+ // Phase 2: Weaponization
92
+ this.currentPhase = 'weaponize';
93
+ this.currentWaveName = 'Weaponization & Strategy Formulation';
94
+ this.addLog(0, 'CHAOS-SYNTHESIZER', `Formulating ${waves.length} assault waves mapped to ${personas.length} active agent personas`, 'info');
95
+ this.emitProgress();
96
+ await this.sleep(400);
97
+ if (this.aborted)
98
+ return this.finalizeAbortedCampaign();
99
+ // Phase 3: Multi-Wave Assault
100
+ this.currentPhase = 'assault';
101
+ for (let i = 0; i < waves.length; i++) {
102
+ if (this.aborted)
103
+ break;
104
+ this.currentWaveIndex = i + 1;
105
+ const wave = waves[i];
106
+ this.currentWaveName = wave.name;
107
+ this.activePersonaName = wave.personaName;
108
+ this.addLog(wave.waveNumber, wave.callSign, `Launching Wave ${wave.waveNumber}/${waves.length}: ${wave.name}`, 'info');
109
+ this.emitWaveStart(wave);
110
+ await this.executeAssaultWave(wave);
111
+ await this.sleep(300);
112
+ }
113
+ if (this.aborted)
114
+ return this.finalizeAbortedCampaign();
115
+ // Phase 4 & 5: Blast Radius Calculation & Debrief
116
+ this.currentPhase = 'debrief';
117
+ this.currentWaveName = 'Blast Radius Analysis & Debrief Compilation';
118
+ this.addLog(0, 'BLAST-ANALYST', 'Quantifying system blast radius and survivability scorecard', 'info');
119
+ this.emitProgress();
120
+ await this.sleep(300);
121
+ return this.finalizeSuccessfulCampaign(waves);
122
+ }
123
+ catch (err) {
124
+ this.addLog(0, 'COMMAND-FAIL', `Campaign execution failure: ${err.message}`, 'warn');
125
+ return this.finalizeAbortedCampaign();
126
+ }
127
+ finally {
128
+ this.cleanupToxics();
129
+ this.active = false;
130
+ }
131
+ }
132
+ abortCampaign() {
133
+ if (!this.active) {
134
+ return { success: false, message: 'No active campaign to abort' };
135
+ }
136
+ this.aborted = true;
137
+ this.currentPhase = 'aborted';
138
+ this.addLog(this.currentWaveIndex, 'COMMAND-ABORT', 'Emergency abort signal received. Neutralizing assault waves and flushing toxic pipeline.', 'warn');
139
+ this.cleanupToxics();
140
+ this.active = false;
141
+ this.emitProgress();
142
+ return { success: true, message: 'Red Team campaign safely aborted' };
143
+ }
144
+ async executeReconPhase() {
145
+ this.addLog(0, 'VULN-HUNTER', `Initiating surface discovery on ${this.targetUrl}`, 'info');
146
+ try {
147
+ const pingRes = await this.dispatchProbe('GET', '/api/data', {}, undefined, 2000);
148
+ this.probesSent++;
149
+ const headers = pingRes.headers || {};
150
+ const serverBanner = headers['server'] || headers['x-powered-by'] || 'Generic / Obfuscated';
151
+ const corsOrigin = headers['access-control-allow-origin'] || 'Restricted / None';
152
+ this.addLog(0, 'VULN-HUNTER', `Target reachable. Server signature: [${serverBanner}], CORS: [${corsOrigin}], Response time: ${pingRes.durationMs}ms`, 'pass');
153
+ // Check defensive headers upfront
154
+ if (!headers['x-content-type-options'] || !headers['x-frame-options']) {
155
+ const breach = {
156
+ id: `br_recon_headers_${Date.now()}`,
157
+ personaId: 'security-reviewer',
158
+ personaName: 'Security Reviewer',
159
+ callSign: 'VULN-HUNTER',
160
+ severity: 'high',
161
+ category: 'Security Perimeter',
162
+ title: 'Missing Baseline Defensive Security Headers',
163
+ impact: 'Exposes target to MIME-sniffing, clickjacking, and origin reflection vulnerabilities.',
164
+ endpoint: '/api/data',
165
+ proofOfBreach: {
166
+ method: 'GET',
167
+ url: `${this.targetUrl}/api/data`,
168
+ responseStatus: pingRes.status,
169
+ responseDurationMs: pingRes.durationMs,
170
+ snippet: `Headers: ${JSON.stringify({ 'x-content-type-options': headers['x-content-type-options'], 'x-frame-options': headers['x-frame-options'] })}`,
171
+ },
172
+ remediation: 'Attach nosniff and DENY frame-options headers to all HTTP responses.',
173
+ autoHealCheckKey: 'Defensive Security Headers',
174
+ };
175
+ this.breaches.push(breach);
176
+ this.emitBreach(breach);
177
+ this.addLog(0, 'VULN-HUNTER', `Breach discovered: ${breach.title} [HIGH]`, 'breach');
178
+ }
179
+ }
180
+ catch (err) {
181
+ this.addLog(0, 'VULN-HUNTER', `Recon warning: Target response irregular (${err.message})`, 'warn');
182
+ }
183
+ }
184
+ async executeAssaultWave(wave) {
185
+ // 1. Inject wave-specific toxics into the middleman proxy
186
+ this.installWaveToxics(wave);
187
+ try {
188
+ switch (wave.id) {
189
+ case 'wave-1-security-recon':
190
+ await this.runSecurityWave(wave);
191
+ break;
192
+ case 'wave-2-transport-stress':
193
+ await this.runTransportStressWave(wave);
194
+ break;
195
+ case 'wave-3-jitter-degrade':
196
+ await this.runJitterDegradeWave(wave);
197
+ break;
198
+ case 'wave-4-adversarial-mutant':
199
+ await this.runAdversarialMutantWave(wave);
200
+ break;
201
+ case 'wave-5-resource-exhaustion':
202
+ await this.runResourceExhaustionWave(wave);
203
+ break;
204
+ default:
205
+ await this.runGenericWave(wave);
206
+ }
207
+ }
208
+ finally {
209
+ this.cleanupToxics();
210
+ }
211
+ }
212
+ async runSecurityWave(wave) {
213
+ // Probe 1: Host Header Poisoning
214
+ const hostRes = await this.dispatchProbe('GET', '/api/data', {
215
+ 'Host': 'attacker-controlled.net',
216
+ 'X-Forwarded-Host': 'attacker-controlled.net',
217
+ });
218
+ this.probesSent++;
219
+ if (hostRes.status === 200) {
220
+ const breach = {
221
+ id: `br_sec_host_${Date.now()}`,
222
+ personaId: wave.personaId,
223
+ personaName: wave.personaName,
224
+ callSign: wave.callSign,
225
+ severity: 'high',
226
+ category: 'Host Validation',
227
+ title: 'Unrestricted Host Header Acceptance',
228
+ impact: 'Server accepted spoofed Host header attacker-controlled.net without validation.',
229
+ endpoint: '/api/data',
230
+ proofOfBreach: {
231
+ method: 'GET',
232
+ url: `${this.targetUrl}/api/data`,
233
+ requestHeaders: { 'Host': 'attacker-controlled.net' },
234
+ responseStatus: hostRes.status,
235
+ responseDurationMs: hostRes.durationMs,
236
+ snippet: hostRes.body.slice(0, 140),
237
+ },
238
+ remediation: 'Enforce strict host whitelist validation in reverse proxy and backend server.',
239
+ autoHealCheckKey: 'Host Header Poisoning & Reflection',
240
+ };
241
+ this.breaches.push(breach);
242
+ this.emitBreach(breach);
243
+ this.addLog(wave.waveNumber, wave.callSign, `Host Header Poisoning probe breached! [HIGH]`, 'breach');
244
+ }
245
+ // Probe 2: Directory Path Traversal
246
+ const travRes = await this.dispatchProbe('GET', '/api/data?file=../../../../etc/passwd', {});
247
+ this.probesSent++;
248
+ if (travRes.status === 200 && !travRes.body.toLowerCase().includes('error')) {
249
+ const breach = {
250
+ id: `br_sec_traversal_${Date.now()}`,
251
+ personaId: wave.personaId,
252
+ personaName: wave.personaName,
253
+ callSign: wave.callSign,
254
+ severity: 'critical',
255
+ category: 'Input Sanitization',
256
+ title: 'Potential Path Traversal & Unsanitized Parameter Acceptance',
257
+ impact: 'Relative path traversal sequences passed without rejection.',
258
+ endpoint: '/api/data?file=../../../../etc/passwd',
259
+ proofOfBreach: {
260
+ method: 'GET',
261
+ url: `${this.targetUrl}/api/data?file=../../../../etc/passwd`,
262
+ responseStatus: travRes.status,
263
+ responseDurationMs: travRes.durationMs,
264
+ snippet: travRes.body.slice(0, 140),
265
+ },
266
+ remediation: 'Sanitize query inputs, strip relative traversal characters, and reject unauthorized path lookups.',
267
+ autoHealCheckKey: 'Path Traversal & Directory Escape (../)',
268
+ };
269
+ this.breaches.push(breach);
270
+ this.emitBreach(breach);
271
+ this.addLog(wave.waveNumber, wave.callSign, `Path Traversal probe accepted! [CRITICAL]`, 'breach');
272
+ }
273
+ // Probe 3: Broken Auth Header / Unhandled Crash
274
+ const authRes = await this.dispatchProbe('GET', '/api/profile', {
275
+ 'Authorization': 'Bearer malformed.invalid.token.signature',
276
+ });
277
+ this.probesSent++;
278
+ if (authRes.status >= 500) {
279
+ const breach = {
280
+ id: `br_sec_authcrash_${Date.now()}`,
281
+ personaId: wave.personaId,
282
+ personaName: wave.personaName,
283
+ callSign: wave.callSign,
284
+ severity: 'critical',
285
+ category: 'Authentication',
286
+ title: 'Server Crash (HTTP 500) on Malformed Authorization Header',
287
+ impact: 'Unsigned or malformed authentication tokens cause internal server unhandled exceptions.',
288
+ endpoint: '/api/profile',
289
+ proofOfBreach: {
290
+ method: 'GET',
291
+ url: `${this.targetUrl}/api/profile`,
292
+ requestHeaders: { 'Authorization': 'Bearer malformed.invalid.token' },
293
+ responseStatus: authRes.status,
294
+ responseDurationMs: authRes.durationMs,
295
+ snippet: authRes.body.slice(0, 140),
296
+ },
297
+ remediation: 'Catch token decoding errors cleanly and return HTTP 401 Unauthorized.',
298
+ autoHealCheckKey: 'Unsigned & Broken Authorization Headers',
299
+ };
300
+ this.breaches.push(breach);
301
+ this.emitBreach(breach);
302
+ this.addLog(wave.waveNumber, wave.callSign, `Server 500 crash on malformed token! [CRITICAL]`, 'breach');
303
+ }
304
+ }
305
+ async runTransportStressWave(wave) {
306
+ // Tests behavior when connection is dropped or cut
307
+ for (let i = 0; i < wave.probesCount; i++) {
308
+ if (this.aborted)
309
+ break;
310
+ const res = await this.dispatchProbe('GET', '/api/data', {}, undefined, 3000);
311
+ this.probesSent++;
312
+ if (res.status === 0 || res.status >= 500) {
313
+ this.addLog(wave.waveNumber, wave.callSign, `Socket dropped mid-download as injected (${res.status || 'ECONNRESET'})`, 'info');
314
+ }
315
+ else if (res.status === 200 && res.body.length < 50) {
316
+ const breach = {
317
+ id: `br_silent_trunc_${Date.now()}`,
318
+ personaId: wave.personaId,
319
+ personaName: wave.personaName,
320
+ callSign: wave.callSign,
321
+ severity: 'high',
322
+ category: 'Stream Resilience',
323
+ title: 'Silent Stream Truncation Accepted as Complete 200 OK',
324
+ impact: 'Server or proxy returned partial body without Content-Length mismatch detection.',
325
+ endpoint: '/api/data',
326
+ proofOfBreach: {
327
+ method: 'GET',
328
+ url: `${this.targetUrl}/api/data`,
329
+ responseStatus: res.status,
330
+ responseDurationMs: res.durationMs,
331
+ snippet: `Truncated response body (${res.body.length} bytes): ${res.body}`,
332
+ },
333
+ remediation: 'Ensure clients and proxy verify complete stream transmission or raise stream error.',
334
+ autoHealCheckKey: 'Payload Truncation & Partial Transfer',
335
+ };
336
+ this.breaches.push(breach);
337
+ this.emitBreach(breach);
338
+ this.addLog(wave.waveNumber, wave.callSign, `Silent truncation breach found! [HIGH]`, 'breach');
339
+ break;
340
+ }
341
+ await this.sleep(100);
342
+ }
343
+ }
344
+ async runJitterDegradeWave(wave) {
345
+ const latencies = [];
346
+ for (let i = 0; i < wave.probesCount; i++) {
347
+ if (this.aborted)
348
+ break;
349
+ const res = await this.dispatchProbe('GET', '/api/users', {}, undefined, 4000);
350
+ this.probesSent++;
351
+ latencies.push(res.durationMs);
352
+ await this.sleep(80);
353
+ }
354
+ const maxLatency = Math.max(...latencies, 0);
355
+ const minLatency = Math.min(...latencies, 0);
356
+ const variance = maxLatency - minLatency;
357
+ this.addLog(wave.waveNumber, wave.callSign, `Jitter analysis: min ${minLatency}ms, max ${maxLatency}ms, spread ${variance}ms`, 'info');
358
+ if (variance > 300) {
359
+ this.addLog(wave.waveNumber, wave.callSign, `Significant jitter fluctuation confirmed (+${variance}ms variance)`, 'warn');
360
+ }
361
+ }
362
+ async runAdversarialMutantWave(wave) {
363
+ // Probe 1: ReDoS Catastrophic Backtracking input
364
+ const redosPayload = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!';
365
+ const redosStart = Date.now();
366
+ const redosRes = await this.dispatchProbe('GET', `/api/data?search=${encodeURIComponent(redosPayload)}`, {}, undefined, 3000);
367
+ this.probesSent++;
368
+ const redosDuration = Date.now() - redosStart;
369
+ if (redosDuration > 1200) {
370
+ const breach = {
371
+ id: `br_gan_redos_${Date.now()}`,
372
+ personaId: wave.personaId,
373
+ personaName: wave.personaName,
374
+ callSign: wave.callSign,
375
+ severity: 'critical',
376
+ category: 'Denial of Service',
377
+ title: 'Suspected Catastrophic Regex Backtracking (ReDoS)',
378
+ impact: `Probe with repetitive pattern required ${redosDuration}ms to evaluate, indicating vulnerable non-linear regex complexity.`,
379
+ endpoint: `/api/data?search=...`,
380
+ proofOfBreach: {
381
+ method: 'GET',
382
+ url: `${this.targetUrl}/api/data?search=${redosPayload}`,
383
+ responseStatus: redosRes.status,
384
+ responseDurationMs: redosDuration,
385
+ snippet: `Execution hung for ${redosDuration}ms`,
386
+ },
387
+ remediation: 'Refactor regex patterns to avoid nested quantifiers or set an atomic timeout guard.',
388
+ autoHealCheckKey: 'ReDoS (Regex Denial of Service) Lockup Probe',
389
+ };
390
+ this.breaches.push(breach);
391
+ this.emitBreach(breach);
392
+ this.addLog(wave.waveNumber, wave.callSign, `ReDoS execution hang detected (${redosDuration}ms)! [CRITICAL]`, 'breach');
393
+ }
394
+ // Probe 2: Concurrency Race Condition (Double-processing test)
395
+ const idempotencyKey = `redteam-idem-${Date.now()}`;
396
+ const racePromises = Array.from({ length: 4 }).map(() => this.dispatchProbe('POST', '/api/checkout', {
397
+ 'Idempotency-Key': idempotencyKey,
398
+ 'Content-Type': 'application/json',
399
+ }, JSON.stringify({ action: 'checkout', amount: 99.95, key: idempotencyKey }), 3000));
400
+ const raceResults = await Promise.all(racePromises);
401
+ this.probesSent += 4;
402
+ const successfulCodes = raceResults.filter(r => r.status === 200 || r.status === 201).length;
403
+ if (successfulCodes > 1) {
404
+ const breach = {
405
+ id: `br_gan_race_${Date.now()}`,
406
+ personaId: wave.personaId,
407
+ personaName: wave.personaName,
408
+ callSign: wave.callSign,
409
+ severity: 'critical',
410
+ category: 'Concurrency Control',
411
+ title: 'Race Condition: Multiple Duplicate State Mutations Processed',
412
+ impact: `${successfulCodes} concurrent requests with identical Idempotency-Key succeeded without deduplication.`,
413
+ endpoint: '/api/checkout',
414
+ proofOfBreach: {
415
+ method: 'POST',
416
+ url: `${this.targetUrl}/api/checkout`,
417
+ requestHeaders: { 'Idempotency-Key': idempotencyKey },
418
+ responseStatus: raceResults[0].status,
419
+ responseDurationMs: raceResults[0].durationMs,
420
+ snippet: `Identical keys accepted ${successfulCodes} times concurrently`,
421
+ },
422
+ remediation: 'Implement atomic distributed locking or transactional database uniqueness constraints.',
423
+ autoHealCheckKey: 'Duplicate Request Idempotency Protection',
424
+ };
425
+ this.breaches.push(breach);
426
+ this.emitBreach(breach);
427
+ this.addLog(wave.waveNumber, wave.callSign, `Idempotency race condition breached (${successfulCodes} duplicate mutations)! [CRITICAL]`, 'breach');
428
+ }
429
+ }
430
+ async runResourceExhaustionWave(wave) {
431
+ // Probe 1: Oversized 1.5MB Body (Checking for HTTP 413)
432
+ const oversizedBody = 'X'.repeat(1.5 * 1024 * 1024);
433
+ const overRes = await this.dispatchProbe('POST', '/api/data', {
434
+ 'Content-Type': 'application/json',
435
+ }, oversizedBody, 4000);
436
+ this.probesSent++;
437
+ if (overRes.status === 200) {
438
+ const breach = {
439
+ id: `br_perf_oversize_${Date.now()}`,
440
+ personaId: wave.personaId,
441
+ personaName: wave.personaName,
442
+ callSign: wave.callSign,
443
+ severity: 'high',
444
+ category: 'Resource Exhaustion',
445
+ title: 'Missing Request Body Size Limit (OOM Vulnerability)',
446
+ impact: 'Server processed 1.5MB payload without enforcing HTTP 413 Payload Too Large.',
447
+ endpoint: '/api/data',
448
+ proofOfBreach: {
449
+ method: 'POST',
450
+ url: `${this.targetUrl}/api/data`,
451
+ responseStatus: overRes.status,
452
+ responseDurationMs: overRes.durationMs,
453
+ snippet: `Allowed payload size: ${oversizedBody.length} bytes`,
454
+ },
455
+ remediation: 'Configure express.json({ limit: "1mb" }) or reverse proxy client_max_body_size.',
456
+ autoHealCheckKey: 'Oversized Payload & Buffer OOM Defense (HTTP 413)',
457
+ };
458
+ this.breaches.push(breach);
459
+ this.emitBreach(breach);
460
+ this.addLog(wave.waveNumber, wave.callSign, `Oversized payload accepted without 413! [HIGH]`, 'breach');
461
+ }
462
+ // Probe 2: Avalanche Concurrency Burst
463
+ const burstCount = this.intensity === 'avalanche' ? 12 : 6;
464
+ const burstPromises = Array.from({ length: burstCount }).map((_, idx) => this.dispatchProbe('GET', `/api/data?burst=${idx}`, {}, undefined, 3000));
465
+ const burstResults = await Promise.all(burstPromises);
466
+ this.probesSent += burstCount;
467
+ const failedBurst = burstResults.filter(r => r.status >= 500 || r.status === 0).length;
468
+ this.addLog(wave.waveNumber, wave.callSign, `Avalanche burst of ${burstCount} concurrent queries completed (${failedBurst} drops)`, 'info');
469
+ }
470
+ async runGenericWave(wave) {
471
+ for (let i = 0; i < wave.probesCount; i++) {
472
+ if (this.aborted)
473
+ break;
474
+ await this.dispatchProbe('GET', wave.targetEndpoint, {}, undefined, 3000);
475
+ this.probesSent++;
476
+ await this.sleep(100);
477
+ }
478
+ }
479
+ installWaveToxics(wave) {
480
+ if (!wave.toxicsToInject || wave.toxicsToInject.length === 0)
481
+ return;
482
+ for (let i = 0; i < wave.toxicsToInject.length; i++) {
483
+ const t = wave.toxicsToInject[i];
484
+ this.pipeline.addRule({
485
+ id: `redteam_wave_${wave.waveNumber}_${t.type}_${i}`,
486
+ name: `RedTeam [${wave.callSign}] ${t.type}`,
487
+ type: t.type,
488
+ direction: t.direction || 'downstream',
489
+ enabled: true,
490
+ pathPattern: t.pathPattern,
491
+ config: t.config,
492
+ });
493
+ }
494
+ }
495
+ cleanupToxics() {
496
+ const rules = this.pipeline.getRules();
497
+ for (const r of rules) {
498
+ if (r.id.startsWith('redteam_')) {
499
+ this.pipeline.removeRule(r.id);
500
+ }
501
+ }
502
+ }
503
+ finalizeSuccessfulCampaign(waves) {
504
+ this.endTime = Date.now();
505
+ this.currentPhase = 'complete';
506
+ // Calculate survivability score (0 - 100)
507
+ let score = 100;
508
+ for (const b of this.breaches) {
509
+ if (b.severity === 'critical')
510
+ score -= 22;
511
+ else if (b.severity === 'high')
512
+ score -= 14;
513
+ else if (b.severity === 'medium')
514
+ score -= 7;
515
+ else
516
+ score -= 3;
517
+ }
518
+ score = Math.max(0, Math.min(100, score));
519
+ let grade = 'F';
520
+ if (score >= 90)
521
+ grade = 'A';
522
+ else if (score >= 80)
523
+ grade = 'B';
524
+ else if (score >= 68)
525
+ grade = 'C';
526
+ else if (score >= 50)
527
+ grade = 'D';
528
+ const activePersonas = Array.from(new Set(waves.map(w => w.personaName)));
529
+ const report = {
530
+ campaignId: this.activeCampaignId || `camp_${Date.now()}`,
531
+ targetUrl: this.targetUrl,
532
+ intensity: this.intensity,
533
+ phase: 'complete',
534
+ startTime: this.startTime,
535
+ endTime: this.endTime,
536
+ durationMs: this.endTime - this.startTime,
537
+ totalWaves: waves.length,
538
+ completedWaves: waves.length,
539
+ totalProbes: this.probesSent,
540
+ breachesFound: [...this.breaches],
541
+ survivabilityScore: score,
542
+ survivabilityGrade: grade,
543
+ activePersonas,
544
+ logs: [...this.logs],
545
+ };
546
+ this.lastReport = report;
547
+ this.addLog(0, 'COMMAND-SUCCESS', `Campaign concluded with score ${score}/100 [Grade ${grade}]. Found ${this.breaches.length} breaches.`, 'pass');
548
+ this.emitComplete(report);
549
+ return report;
550
+ }
551
+ finalizeAbortedCampaign() {
552
+ this.endTime = Date.now();
553
+ this.currentPhase = 'aborted';
554
+ const report = {
555
+ campaignId: this.activeCampaignId || `camp_${Date.now()}`,
556
+ targetUrl: this.targetUrl,
557
+ intensity: this.intensity,
558
+ phase: 'aborted',
559
+ startTime: this.startTime,
560
+ endTime: this.endTime,
561
+ durationMs: this.endTime - this.startTime,
562
+ totalWaves: this.totalWaves,
563
+ completedWaves: this.currentWaveIndex,
564
+ totalProbes: this.probesSent,
565
+ breachesFound: [...this.breaches],
566
+ survivabilityScore: 0,
567
+ survivabilityGrade: 'F',
568
+ activePersonas: [],
569
+ logs: [...this.logs],
570
+ };
571
+ this.lastReport = report;
572
+ return report;
573
+ }
574
+ async dispatchProbe(method, endpoint, headers, body, timeoutMs = 3000) {
575
+ const fullUrl = new URL(endpoint, this.targetUrl);
576
+ const start = Date.now();
577
+ return new Promise((resolve) => {
578
+ let isSettled = false;
579
+ const parsedPort = fullUrl.port ? parseInt(fullUrl.port, 10) : (fullUrl.protocol === 'https:' ? 443 : 80);
580
+ const safeResolve = (result) => {
581
+ if (isSettled)
582
+ return;
583
+ isSettled = true;
584
+ clearTimeout(hardDeadlineTimer);
585
+ resolve(result);
586
+ };
587
+ // Hard safety timer to prevent any probe from hanging under unhandled socket drops or TCP starvation
588
+ const hardDeadlineTimer = setTimeout(() => {
589
+ try {
590
+ req.destroy();
591
+ }
592
+ catch { }
593
+ safeResolve({
594
+ status: 504,
595
+ durationMs: Date.now() - start,
596
+ headers: {},
597
+ body: 'Gateway Timeout (Probe hard deadline exceeded)',
598
+ });
599
+ }, timeoutMs + 100);
600
+ const req = http.request({
601
+ hostname: fullUrl.hostname,
602
+ port: parsedPort,
603
+ path: fullUrl.pathname + fullUrl.search,
604
+ method,
605
+ headers: {
606
+ 'User-Agent': 'FaultMesh-RedTeam-Engine/1.0',
607
+ 'Accept': 'application/json, text/plain, */*',
608
+ ...headers,
609
+ },
610
+ timeout: timeoutMs,
611
+ }, (res) => {
612
+ let resBody = '';
613
+ res.setEncoding('utf-8');
614
+ res.on('data', chunk => { resBody += chunk; });
615
+ res.on('end', () => {
616
+ const flatHeaders = {};
617
+ for (const [k, v] of Object.entries(res.headers)) {
618
+ if (v)
619
+ flatHeaders[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : v;
620
+ }
621
+ safeResolve({
622
+ status: res.statusCode || 0,
623
+ durationMs: Date.now() - start,
624
+ headers: flatHeaders,
625
+ body: resBody,
626
+ });
627
+ });
628
+ // Handle socket severance mid-stream (e.g. CutToxic / ECONNRESET)
629
+ res.on('error', (err) => {
630
+ safeResolve({
631
+ status: 0,
632
+ durationMs: Date.now() - start,
633
+ headers: {},
634
+ body: `Socket error mid-stream: ${err.message}`,
635
+ });
636
+ });
637
+ res.on('close', () => {
638
+ if (!res.complete) {
639
+ safeResolve({
640
+ status: 0,
641
+ durationMs: Date.now() - start,
642
+ headers: {},
643
+ body: 'Connection severed abruptly mid-transfer (ECONNRESET)',
644
+ });
645
+ }
646
+ });
647
+ });
648
+ req.on('timeout', () => {
649
+ try {
650
+ req.destroy();
651
+ }
652
+ catch { }
653
+ safeResolve({
654
+ status: 504,
655
+ durationMs: Date.now() - start,
656
+ headers: {},
657
+ body: 'Gateway Timeout (Probe deadline exceeded)',
658
+ });
659
+ });
660
+ req.on('error', (err) => {
661
+ safeResolve({
662
+ status: 0,
663
+ durationMs: Date.now() - start,
664
+ headers: {},
665
+ body: `Socket error: ${err.message}`,
666
+ });
667
+ });
668
+ if (body) {
669
+ req.write(body);
670
+ }
671
+ req.end();
672
+ });
673
+ }
674
+ addLog(waveNumber, persona, message, type) {
675
+ const entry = {
676
+ timestamp: Date.now(),
677
+ waveNumber,
678
+ persona,
679
+ message,
680
+ type,
681
+ };
682
+ this.logs.push(entry);
683
+ this.telemetryHub.broadcastCustomEvent('redteam-log', entry);
684
+ }
685
+ emitProgress() {
686
+ this.telemetryHub.broadcastCustomEvent('redteam-progress', this.getStatus());
687
+ }
688
+ emitWaveStart(wave) {
689
+ this.telemetryHub.broadcastCustomEvent('redteam-wave-start', {
690
+ wave,
691
+ status: this.getStatus(),
692
+ });
693
+ }
694
+ emitBreach(breach) {
695
+ this.telemetryHub.broadcastCustomEvent('redteam-breach', {
696
+ breach,
697
+ status: this.getStatus(),
698
+ });
699
+ }
700
+ emitComplete(report) {
701
+ this.telemetryHub.broadcastCustomEvent('redteam-complete', {
702
+ report,
703
+ status: this.getStatus(),
704
+ });
705
+ }
706
+ sleep(ms) {
707
+ return new Promise(r => setTimeout(r, ms));
708
+ }
709
+ }