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
@@ -20,8 +20,11 @@ document.addEventListener('DOMContentLoaded', () => {
20
20
  const btnCloseResponse = document.getElementById('btnCloseResponse');
21
21
 
22
22
  const btnPresetSlowMobile = document.getElementById('btnPresetSlowMobile');
23
+ const btnPresetJitter = document.getElementById('btnPresetJitter');
23
24
  const btnPresetDisconnect = document.getElementById('btnPresetDisconnect');
25
+ const btnPresetRateLimit = document.getElementById('btnPresetRateLimit');
24
26
  const btnPresetOutage = document.getElementById('btnPresetOutage');
27
+ const btnPresetGatewayTimeout = document.getElementById('btnPresetGatewayTimeout');
25
28
  const btnPresetCorrupt = document.getElementById('btnPresetCorrupt');
26
29
  const btnClearAllRules = document.getElementById('btnClearAllRules');
27
30
 
@@ -46,12 +49,77 @@ document.addEventListener('DOMContentLoaded', () => {
46
49
  const scoreSubtitle = document.getElementById('scoreSubtitle');
47
50
  const diagnosticsResults = document.getElementById('diagnosticsResults');
48
51
 
52
+ const targetUrlInput = document.getElementById('targetUrlInput');
53
+ const btnSetTargetUrl = document.getElementById('btnSetTargetUrl');
54
+ const exportGroup = document.getElementById('exportGroup');
55
+ const btnExportMd = document.getElementById('btnExportMd');
56
+ const btnExportJson = document.getElementById('btnExportJson');
57
+ const rulePathPattern = document.getElementById('rulePathPattern');
58
+
59
+ // Auto-Healer Modal Elements
60
+ const btnOpenHealer = document.getElementById('btnOpenHealer');
61
+ const healerModal = document.getElementById('healerModal');
62
+ const btnCloseHealer = document.getElementById('btnCloseHealer');
63
+ const healerProjectDir = document.getElementById('healerProjectDir');
64
+ const btnHealerScan = document.getElementById('btnHealerScan');
65
+ const healerScanStatus = document.getElementById('healerScanStatus');
66
+ const healerDiffContainer = document.getElementById('healerDiffContainer');
67
+ const healerActionsBar = document.getElementById('healerActionsBar');
68
+ const healerBackupCheck = document.getElementById('healerBackupCheck');
69
+ const btnHealerApply = document.getElementById('btnHealerApply');
70
+ const btnHealerRollback = document.getElementById('btnHealerRollback');
71
+ const btnToggleAiSettings = document.getElementById('btnToggleAiSettings');
72
+ const healerAiSettingsPanel = document.getElementById('healerAiSettingsPanel');
73
+ const healerEngineBadge = document.getElementById('healerEngineBadge');
74
+ const healerEngineMode = document.getElementById('healerEngineMode');
75
+ const healerAiProvider = document.getElementById('healerAiProvider');
76
+ const healerAiKey = document.getElementById('healerAiKey');
77
+ const healerAiEndpoint = document.getElementById('healerAiEndpoint');
78
+ const aiKeyGroup = document.getElementById('aiKeyGroup');
79
+ const aiEndpointGroup = document.getElementById('aiEndpointGroup');
80
+
49
81
  const activityFeed = document.getElementById('activityFeed');
50
82
  const btnClearLog = document.getElementById('btnClearLog');
51
83
 
84
+ // Red Team Tactical Console Elements
85
+ const tabRedTeam = document.getElementById('tabRedTeam');
86
+ const standardAuditPanel = document.getElementById('standardAuditPanel');
87
+ const redTeamPanel = document.getElementById('redTeamPanel');
88
+ const redTeamPersonasGrid = document.getElementById('redTeamPersonasGrid');
89
+ const redTeamIntensitySelect = document.getElementById('redTeamIntensitySelect');
90
+ const btnLaunchRedTeam = document.getElementById('btnLaunchRedTeam');
91
+ const btnAbortRedTeam = document.getElementById('btnAbortRedTeam');
92
+ const redTeamHud = document.getElementById('redTeamHud');
93
+ const redTeamCurrentWaveText = document.getElementById('redTeamCurrentWaveText');
94
+ const redTeamProgressPercent = document.getElementById('redTeamProgressPercent');
95
+ const redTeamProgressBar = document.getElementById('redTeamProgressBar');
96
+ const redTeamProbesCount = document.getElementById('redTeamProbesCount');
97
+ const redTeamCritCount = document.getElementById('redTeamCritCount');
98
+ const redTeamHighCount = document.getElementById('redTeamHighCount');
99
+ const redTeamMedCount = document.getElementById('redTeamMedCount');
100
+ const redTeamActivePersonaPill = document.getElementById('redTeamActivePersonaPill');
101
+ const redTeamConsoleLogs = document.getElementById('redTeamConsoleLogs');
102
+ const redTeamDossier = document.getElementById('redTeamDossier');
103
+ const dossierScoreVal = document.getElementById('dossierScoreVal');
104
+ const dossierGradeVal = document.getElementById('dossierGradeVal');
105
+ const dossierTitle = document.getElementById('dossierTitle');
106
+ const dossierSummaryText = document.getElementById('dossierSummaryText');
107
+ const dossierFindingsCount = document.getElementById('dossierFindingsCount');
108
+ const dossierFindingsList = document.getElementById('dossierFindingsList');
109
+ const btnAutoHealBreaches = document.getElementById('btnAutoHealBreaches');
110
+ const btnExportDossierMd = document.getElementById('btnExportDossierMd');
111
+ const btnExportDossierJson = document.getElementById('btnExportDossierJson');
112
+
113
+ let lastRedTeamReport = null;
114
+ let redTeamPollingInterval = null;
115
+
116
+ let lastHealerScanResult = null;
117
+ let lastBackupDir = null;
118
+
52
119
  let activeRules = [];
53
120
  let isRunningDiagnostics = false;
54
121
  let currentDiagMode = 'resilience';
122
+ let lastScorecardData = null;
55
123
 
56
124
  // 1. Theme Management (Clean Light / Neutral Dark)
57
125
  function initTheme() {
@@ -67,6 +135,73 @@ document.addEventListener('DOMContentLoaded', () => {
67
135
  });
68
136
  initTheme();
69
137
 
138
+ // Target URL Management
139
+ async function loadTargetUrl() {
140
+ try {
141
+ const res = await fetch('/_faultmesh/config/target');
142
+ const data = await res.json();
143
+ if (data.targetUrl && targetUrlInput) {
144
+ targetUrlInput.value = data.targetUrl;
145
+ }
146
+ } catch (err) {
147
+ console.warn('Failed to load target URL:', err);
148
+ }
149
+ }
150
+
151
+ async function syncTargetUrl() {
152
+ if (!targetUrlInput) return;
153
+ const targetUrl = targetUrlInput.value.trim();
154
+ if (!targetUrl) return;
155
+ try {
156
+ await fetch('/_faultmesh/config/target', {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ targetUrl }),
160
+ });
161
+ } catch (err) {
162
+ console.warn('Failed to auto-sync target URL:', err);
163
+ }
164
+ }
165
+
166
+ if (btnSetTargetUrl && targetUrlInput) {
167
+ targetUrlInput.addEventListener('keydown', (e) => {
168
+ if (e.key === 'Enter') {
169
+ e.preventDefault();
170
+ btnSetTargetUrl.click();
171
+ }
172
+ });
173
+
174
+ btnSetTargetUrl.addEventListener('click', async () => {
175
+ const targetUrl = targetUrlInput.value.trim();
176
+ if (!targetUrl) return;
177
+ btnSetTargetUrl.disabled = true;
178
+ btnSetTargetUrl.textContent = 'Updating...';
179
+ try {
180
+ const res = await fetch('/_faultmesh/config/target', {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({ targetUrl }),
184
+ });
185
+ const data = await res.json();
186
+ if (res.ok && data.success) {
187
+ btnSetTargetUrl.textContent = 'Updated!';
188
+ setTimeout(() => {
189
+ btnSetTargetUrl.textContent = 'Set Target';
190
+ btnSetTargetUrl.disabled = false;
191
+ }, 1500);
192
+ } else {
193
+ alert('Error: ' + (data.details || data.error || 'Failed to update target URL'));
194
+ btnSetTargetUrl.textContent = 'Set Target';
195
+ btnSetTargetUrl.disabled = false;
196
+ }
197
+ } catch (err) {
198
+ alert('Network error: ' + err.message);
199
+ btnSetTargetUrl.textContent = 'Set Target';
200
+ btnSetTargetUrl.disabled = false;
201
+ }
202
+ });
203
+ }
204
+
70
205
  // 2. Response Inspector Panel
71
206
  if (btnCloseResponse) {
72
207
  btnCloseResponse.addEventListener('click', () => {
@@ -75,7 +210,7 @@ document.addEventListener('DOMContentLoaded', () => {
75
210
  respStatusBadge.className = 'status-pill';
76
211
  }
77
212
  if (respDuration) respDuration.textContent = '-- ms';
78
- if (respAppliedRule) respAppliedRule.textContent = 'Rule: None';
213
+ if (respAppliedRule) respAppliedRule.textContent = 'None';
79
214
  if (testFeedback) {
80
215
  testFeedback.textContent = 'Click "Send Test Request" or enable any scenario on the left to inspect responses.';
81
216
  testFeedback.style.color = 'var(--text-muted)';
@@ -93,8 +228,16 @@ document.addEventListener('DOMContentLoaded', () => {
93
228
  respStatusBadge.className = 'status-pill ' + (isError || status >= 500 ? 'code-5xx' : (status >= 400 ? 'code-4xx' : 'code-2xx'));
94
229
  respDuration.textContent = `${durationMs}ms`;
95
230
 
96
- const ruleNames = appliedRules && appliedRules.length > 0 ? appliedRules.join(', ') : 'None (Normal Traffic)';
97
- respAppliedRule.textContent = `Applied: ${ruleNames}`;
231
+ if (!appliedRules || appliedRules.length === 0) {
232
+ respAppliedRule.textContent = 'None';
233
+ respAppliedRule.removeAttribute('title');
234
+ } else if (appliedRules.length === 1) {
235
+ respAppliedRule.textContent = appliedRules[0];
236
+ respAppliedRule.title = `Injected: ${appliedRules[0]}`;
237
+ } else {
238
+ respAppliedRule.textContent = `${appliedRules[0]} (+${appliedRules.length - 1} more)`;
239
+ respAppliedRule.title = `Injected Faults:\n${appliedRules.map((r, i) => `${i + 1}. ${r}`).join('\n')}`;
240
+ }
98
241
 
99
242
  // Format body as formatted JSON if possible
100
243
  try {
@@ -208,10 +351,15 @@ document.addEventListener('DOMContentLoaded', () => {
208
351
  if (activeRules.length === 0) {
209
352
  activeRulesIndicator.textContent = 'Traffic: Normal';
210
353
  activeRulesIndicator.className = 'active-rules-pill';
354
+ activeRulesIndicator.removeAttribute('title');
355
+ } else if (activeRules.length === 1) {
356
+ activeRulesIndicator.textContent = `Simulating: ${activeRules[0].name}`;
357
+ activeRulesIndicator.className = 'active-rules-pill active';
358
+ activeRulesIndicator.title = `Simulating: ${activeRules[0].name}`;
211
359
  } else {
212
- const names = activeRules.map(r => r.name).join(', ');
213
- activeRulesIndicator.textContent = `Simulating: ${names}`;
360
+ activeRulesIndicator.textContent = `Simulating: ${activeRules[0].name} (+${activeRules.length - 1} more)`;
214
361
  activeRulesIndicator.className = 'active-rules-pill active';
362
+ activeRulesIndicator.title = `Active Simulation Rules:\n${activeRules.map((r, i) => `${i + 1}. ${r.name}`).join('\n')}`;
215
363
  }
216
364
  }
217
365
 
@@ -237,14 +385,19 @@ document.addEventListener('DOMContentLoaded', () => {
237
385
  function formatRuleDetail(r) {
238
386
  const c = r.config || {};
239
387
  const dir = r.direction === 'downstream' ? 'Responses' : 'Requests';
388
+ let detail = '';
240
389
  switch (r.type) {
241
- case 'latency': return `${dir} delayed +${c.latencyMs}ms (${c.jitterMs ? `±${c.jitterMs}ms` : 'fixed'})`;
242
- case 'bandwidth': return `${dir} capped at ${c.rateKbps} kbps`;
243
- case 'cut': return `Socket dropped after ${c.cutAfterBytes} bytes`;
244
- case 'corrupt': return `Corrupted response (${c.corruptType || 'truncate'})`;
245
- case 'status': return `Returning HTTP ${c.statusCode}`;
246
- default: return JSON.stringify(c);
390
+ case 'latency': detail = `${dir} delayed +${c.latencyMs}ms (${c.jitterMs ? `±${c.jitterMs}ms` : 'fixed'})`; break;
391
+ case 'bandwidth': detail = `${dir} capped at ${c.rateKbps} kbps`; break;
392
+ case 'cut': detail = `Socket dropped after ${c.cutAfterBytes} bytes`; break;
393
+ case 'corrupt': detail = `Corrupted response (${c.corruptType || 'truncate'})`; break;
394
+ case 'status': detail = `Returning HTTP ${c.statusCode}`; break;
395
+ default: detail = JSON.stringify(c);
396
+ }
397
+ if (r.pathPattern) {
398
+ detail += ` | Path: ${r.pathPattern}`;
247
399
  }
400
+ return detail;
248
401
  }
249
402
 
250
403
  window.deleteRule = async function(id) {
@@ -306,11 +459,16 @@ document.addEventListener('DOMContentLoaded', () => {
306
459
  break;
307
460
  }
308
461
 
462
+ const pathPattern = rulePathPattern ? (rulePathPattern.value.trim() || undefined) : undefined;
463
+ if (pathPattern) {
464
+ name += ` (${pathPattern})`;
465
+ }
466
+
309
467
  try {
310
468
  await fetch('/_faultmesh/rules', {
311
469
  method: 'POST',
312
470
  headers: { 'Content-Type': 'application/json' },
313
- body: JSON.stringify({ id, name, type, direction, enabled: true, config }),
471
+ body: JSON.stringify({ id, name, type, direction, enabled: true, config, pathPattern }),
314
472
  });
315
473
  await refreshStatus();
316
474
  executeTestRequest();
@@ -365,7 +523,7 @@ document.addEventListener('DOMContentLoaded', () => {
365
523
  method: 'POST',
366
524
  headers: { 'Content-Type': 'application/json' },
367
525
  body: JSON.stringify({
368
- id: `preset_bw_${Date.now()}`,
526
+ id: 'preset_slow_mobile_bw',
369
527
  name: 'Slow Mobile Internet (16 kbps limit)',
370
528
  type: 'bandwidth',
371
529
  direction: 'downstream',
@@ -377,7 +535,7 @@ document.addEventListener('DOMContentLoaded', () => {
377
535
  method: 'POST',
378
536
  headers: { 'Content-Type': 'application/json' },
379
537
  body: JSON.stringify({
380
- id: `preset_lat_${Date.now()}`,
538
+ id: 'preset_slow_mobile_lat',
381
539
  name: 'Mobile Delay (350ms)',
382
540
  type: 'latency',
383
541
  direction: 'downstream',
@@ -389,12 +547,31 @@ document.addEventListener('DOMContentLoaded', () => {
389
547
  executeTestRequest();
390
548
  });
391
549
 
550
+ if (btnPresetJitter) {
551
+ btnPresetJitter.addEventListener('click', async () => {
552
+ await fetch('/_faultmesh/rules', {
553
+ method: 'POST',
554
+ headers: { 'Content-Type': 'application/json' },
555
+ body: JSON.stringify({
556
+ id: 'preset_jitter_lat',
557
+ name: 'Packet Jitter Spike (400ms ±250ms)',
558
+ type: 'latency',
559
+ direction: 'downstream',
560
+ enabled: true,
561
+ config: { latencyMs: 400, jitterMs: 250 },
562
+ }),
563
+ });
564
+ await refreshStatus();
565
+ executeTestRequest();
566
+ });
567
+ }
568
+
392
569
  btnPresetDisconnect.addEventListener('click', async () => {
393
570
  await fetch('/_faultmesh/rules', {
394
571
  method: 'POST',
395
572
  headers: { 'Content-Type': 'application/json' },
396
573
  body: JSON.stringify({
397
- id: `preset_cut_${Date.now()}`,
574
+ id: 'preset_disconnect_cut',
398
575
  name: 'Connection Drop (after 30 bytes)',
399
576
  type: 'cut',
400
577
  direction: 'downstream',
@@ -406,12 +583,31 @@ document.addEventListener('DOMContentLoaded', () => {
406
583
  executeTestRequest();
407
584
  });
408
585
 
586
+ if (btnPresetRateLimit) {
587
+ btnPresetRateLimit.addEventListener('click', async () => {
588
+ await fetch('/_faultmesh/rules', {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json' },
591
+ body: JSON.stringify({
592
+ id: 'preset_ratelimit_status',
593
+ name: 'Rate Limit Surge (HTTP 429)',
594
+ type: 'status',
595
+ direction: 'downstream',
596
+ enabled: true,
597
+ config: { statusCode: 429, statusMessage: 'Too Many Requests (Rate Limited - Retry After 15s)' },
598
+ }),
599
+ });
600
+ await refreshStatus();
601
+ executeTestRequest();
602
+ });
603
+ }
604
+
409
605
  btnPresetOutage.addEventListener('click', async () => {
410
606
  await fetch('/_faultmesh/rules', {
411
607
  method: 'POST',
412
608
  headers: { 'Content-Type': 'application/json' },
413
609
  body: JSON.stringify({
414
- id: `preset_503_${Date.now()}`,
610
+ id: 'preset_outage_status',
415
611
  name: 'Server Outage (HTTP 503)',
416
612
  type: 'status',
417
613
  direction: 'downstream',
@@ -423,12 +619,43 @@ document.addEventListener('DOMContentLoaded', () => {
423
619
  executeTestRequest();
424
620
  });
425
621
 
622
+ if (btnPresetGatewayTimeout) {
623
+ btnPresetGatewayTimeout.addEventListener('click', async () => {
624
+ await fetch('/_faultmesh/rules', {
625
+ method: 'POST',
626
+ headers: { 'Content-Type': 'application/json' },
627
+ body: JSON.stringify({
628
+ id: 'preset_gateway_lat',
629
+ name: 'Upstream Delay (2000ms)',
630
+ type: 'latency',
631
+ direction: 'downstream',
632
+ enabled: true,
633
+ config: { latencyMs: 2000, jitterMs: 0 },
634
+ }),
635
+ });
636
+ await fetch('/_faultmesh/rules', {
637
+ method: 'POST',
638
+ headers: { 'Content-Type': 'application/json' },
639
+ body: JSON.stringify({
640
+ id: 'preset_gateway_status',
641
+ name: 'Gateway Timeout (HTTP 504)',
642
+ type: 'status',
643
+ direction: 'downstream',
644
+ enabled: true,
645
+ config: { statusCode: 504, statusMessage: 'Gateway Timeout (Upstream Deadlock)' },
646
+ }),
647
+ });
648
+ await refreshStatus();
649
+ executeTestRequest();
650
+ });
651
+ }
652
+
426
653
  btnPresetCorrupt.addEventListener('click', async () => {
427
654
  await fetch('/_faultmesh/rules', {
428
655
  method: 'POST',
429
656
  headers: { 'Content-Type': 'application/json' },
430
657
  body: JSON.stringify({
431
- id: `preset_corrupt_${Date.now()}`,
658
+ id: 'preset_corrupt_data',
432
659
  name: 'Corrupted Response (Truncated JSON)',
433
660
  type: 'corrupt',
434
661
  direction: 'downstream',
@@ -440,7 +667,7 @@ document.addEventListener('DOMContentLoaded', () => {
440
667
  executeTestRequest();
441
668
  });
442
669
 
443
- // 8. Diagnostics & Security Audit Suite Definitions
670
+ // 8. Diagnostics & Security Audit Suite Definitions (32 Checks Total)
444
671
  const RESILIENCE_CHECKS = [
445
672
  {
446
673
  num: '01',
@@ -476,6 +703,41 @@ document.addEventListener('DOMContentLoaded', () => {
476
703
  category: 'Availability',
477
704
  description: 'Tests if the application properly receives, traps, and gracefully responds to temporary upstream server downtime.',
478
705
  probe: 'Forces proxy to return HTTP 503 Service Unavailable.'
706
+ },
707
+ {
708
+ num: '06',
709
+ name: 'Packet Jitter & Latency Variance',
710
+ category: 'Jitter',
711
+ description: 'Injects fluctuating network latency variance (100ms-350ms) to evaluate client jitter buffering and prevent timeout crashes.',
712
+ probe: 'Applies latency with dynamic 70ms jitter variance.'
713
+ },
714
+ {
715
+ num: '07',
716
+ name: 'Half-Open Circuit Breaker Recovery',
717
+ category: 'Resilience',
718
+ description: 'Tests if client circuit breaker automatically recovers after a transient outage lifts without staying permanently latched in failure state.',
719
+ probe: 'Injects a transient 503 outage followed by immediate recovery probe.'
720
+ },
721
+ {
722
+ num: '08',
723
+ name: 'Downstream Socket Starvation & Slow Read',
724
+ category: 'Concurrency',
725
+ description: 'Evaluates if slow downstream readers starve the server worker pool or block concurrent health probe requests.',
726
+ probe: 'Holds slow stream at 8 kbps while concurrently probing /api/health.'
727
+ },
728
+ {
729
+ num: '09',
730
+ name: 'Zombie Connection Leak Probe',
731
+ category: 'Resources',
732
+ description: 'Verifies backend closes downstream descriptors and frees resources cleanly when client connections abort prematurely.',
733
+ probe: 'Dispatches aborted client socket and measures server recovery latency.'
734
+ },
735
+ {
736
+ num: '10',
737
+ name: 'Payload Truncation & Partial Transfer',
738
+ category: 'Integrity',
739
+ description: 'Tests whether clients safely identify incomplete HTTP body streams rather than treating truncated responses as valid.',
740
+ probe: 'Truncates TCP stream after 30 bytes to test stream termination validation.'
479
741
  }
480
742
  ];
481
743
 
@@ -535,6 +797,62 @@ document.addEventListener('DOMContentLoaded', () => {
535
797
  severity: 'critical',
536
798
  description: 'Probes input fields using harmless syntax markers (\' OR \'1\'=\'1) and non-executable canary tags (<faultmesh-canary-test>) to verify parameterization with 0 database mutation.',
537
799
  probe: 'Zero-damage canary probe verifying parameter escaping and schema validation.'
800
+ },
801
+ {
802
+ num: '08',
803
+ name: 'Host Header Poisoning & Reflection',
804
+ category: 'Headers',
805
+ severity: 'high',
806
+ description: 'Tests if server validates incoming Host and X-Forwarded-Host headers or echoes untrusted attacker domains into redirect URLs.',
807
+ probe: 'Inspects location redirects and response headers under spoofed Host headers.'
808
+ },
809
+ {
810
+ num: '09',
811
+ name: 'Client IP Spoofing & Rate-Limit Bypass',
812
+ category: 'Identity',
813
+ severity: 'medium',
814
+ description: 'Checks whether client IP determination blindly trusts unverified X-Forwarded-For headers to bypass rate limiters or security filters.',
815
+ probe: 'Dispatches forged upstream IP headers to evaluate trusted proxy configuration.'
816
+ },
817
+ {
818
+ num: '10',
819
+ name: 'HTTP Parameter Pollution (HPP)',
820
+ category: 'Parameters',
821
+ severity: 'medium',
822
+ description: 'Tests if server safely handles duplicate query parameters (?id=1&id=2) without array type confusion or unhandled 500 crashes.',
823
+ probe: 'Probes duplicate query parameters across API endpoints.'
824
+ },
825
+ {
826
+ num: '11',
827
+ name: 'Sensitive Cache-Control Verification',
828
+ category: 'Caching',
829
+ severity: 'high',
830
+ description: 'Verifies that endpoints returning authenticated or private user data enforce Cache-Control: no-store to prevent shared proxy and browser cache leaks.',
831
+ probe: 'Inspects Cache-Control and Pragma response directives on user routes.'
832
+ },
833
+ {
834
+ num: '12',
835
+ name: 'Unsigned & Broken Authorization Headers',
836
+ category: 'Auth Safety',
837
+ severity: 'high',
838
+ description: 'Tests if server safely rejects malformed or truncated Authorization headers (HTTP 401/400) without unhandled 500 crashes.',
839
+ probe: 'Submits malformed Bearer tokens to verify graceful rejection.'
840
+ },
841
+ {
842
+ num: '13',
843
+ name: 'Constant-Time Authentication & Timing Attacks',
844
+ category: 'Cryptography',
845
+ severity: 'medium',
846
+ description: 'Tests if credential and token verification exhibits significant latency variations (side-channel timing leaks).',
847
+ probe: 'Measures delta latency across varying token lengths to verify constant-time evaluation.'
848
+ },
849
+ {
850
+ num: '14',
851
+ name: 'Server Metadata & Secret Configuration Exposure',
852
+ category: 'Config Exposure',
853
+ severity: 'critical',
854
+ description: 'Probes for unintentional exposure of sensitive server files such as .env, .git, or unauthenticated internal configuration routes.',
855
+ probe: 'Probes /.env and dotfiles to confirm web server blocks configuration exposure.'
538
856
  }
539
857
  ];
540
858
 
@@ -570,6 +888,38 @@ document.addEventListener('DOMContentLoaded', () => {
570
888
  severity: 'critical',
571
889
  description: 'Verifies that concurrent duplicate POST requests sharing an Idempotency-Key are deduplicated to prevent double-billing and duplicate records.',
572
890
  probe: 'Dispatches concurrent POST requests with duplicate Idempotency-Key headers.'
891
+ },
892
+ {
893
+ num: '05',
894
+ name: 'ReDoS (Regex Denial of Service) Lockup Probe',
895
+ category: 'Regex Safety',
896
+ severity: 'critical',
897
+ description: 'Probes input fields with catastrophic backtracking pattern inputs to verify regular expressions do not lock up the server CPU event loop.',
898
+ probe: 'Sends nested quantifier string to test regex evaluation bounds.'
899
+ },
900
+ {
901
+ num: '06',
902
+ name: 'Concurrent Race Condition & Double Processing',
903
+ category: 'Concurrency',
904
+ severity: 'critical',
905
+ description: 'Dispatches concurrent parallel requests against transactional endpoints to evaluate atomic locking and double-spend protection.',
906
+ probe: 'Executes parallel burst with identical transaction keys to test lock safety.'
907
+ },
908
+ {
909
+ num: '07',
910
+ name: 'Chunked Request Drip & Slow POST Defense',
911
+ category: 'Body Timeouts',
912
+ severity: 'high',
913
+ description: 'Evaluates server defense against Slow POST drip attacks by checking requestTimeout limits on incomplete body deliveries.',
914
+ probe: 'Probes request body reception bounds with delayed chunk transfer.'
915
+ },
916
+ {
917
+ num: '08',
918
+ name: 'Resource Avalanche & Sudden Concurrency Flood',
919
+ category: 'Queue Saturation',
920
+ severity: 'high',
921
+ description: 'Sends an avalanche burst of parallel requests within a 50ms window to verify connection queuing and sub-second mean response latency.',
922
+ probe: 'Dispatches burst concurrency flood to test queue shedding and keep-alive stability.'
573
923
  }
574
924
  ];
575
925
 
@@ -609,46 +959,62 @@ document.addEventListener('DOMContentLoaded', () => {
609
959
  if (tabResilience) tabResilience.classList.remove('active');
610
960
  if (tabSecurity) tabSecurity.classList.remove('active');
611
961
  if (tabStorm) tabStorm.classList.remove('active');
962
+ if (tabRedTeam) tabRedTeam.classList.remove('active');
963
+ if (exportGroup) exportGroup.style.display = 'none';
964
+
965
+ if (mode === 'redteam') {
966
+ if (tabRedTeam) tabRedTeam.classList.add('active');
967
+ if (standardAuditPanel) standardAuditPanel.style.display = 'none';
968
+ if (redTeamPanel) redTeamPanel.style.display = 'flex';
969
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Autonomous Red Chaos Team Engine';
970
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Deploy autonomous agent personas armed with multi-wave fuzzing, compound network fault injections, and system survivability scoring.';
971
+ loadRedTeamPersonas();
972
+ refreshRedTeamStatus();
973
+ return;
974
+ }
975
+
976
+ if (standardAuditPanel) standardAuditPanel.style.display = 'block';
977
+ if (redTeamPanel) redTeamPanel.style.display = 'none';
612
978
 
613
979
  if (mode === 'storm') {
614
980
  if (tabStorm) tabStorm.classList.add('active');
615
- if (diagHeaderTitle) diagHeaderTitle.textContent = 'Traffic Storms & DoS Defense Suite';
616
- if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Evaluates rate limit backoff (429), oversized payload protection (413), slowloris drips, and idempotency deduplication.';
981
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Traffic Storms & DoS Defense Suite (8 Checks)';
982
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Evaluates rate limit backoff (429), oversized payloads (413), slowloris drips, idempotency, ReDoS lockup, race conditions, and avalanche floods.';
617
983
  if (testTargetProfile) {
618
984
  testTargetProfile.innerHTML = `
619
985
  <option value="resilient">Target: Resilient System (Grade A)</option>
620
986
  <option value="fragile">Target: Fragile System (Grade F)</option>
621
987
  `;
622
988
  }
623
- if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run Traffic Storm Benchmark';
989
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 8-Point Storm Benchmark';
624
990
  scoreTitle.textContent = 'Ready for Traffic Storms & DoS evaluation';
625
- scoreSubtitle.textContent = 'Click "Run Traffic Storm Benchmark" to test rate limit backoff, slowloris defense, and idempotency.';
991
+ scoreSubtitle.textContent = 'Click "Run 8-Point Storm Benchmark" to test rate limit backoff, slowloris defense, ReDoS, and concurrency floods.';
626
992
  } else if (mode === 'security') {
627
993
  if (tabSecurity) tabSecurity.classList.add('active');
628
- if (diagHeaderTitle) diagHeaderTitle.textContent = 'Security & Protocol Audit';
629
- if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Non-destructive 7-point audit evaluating defensive headers, CORS, URL tokens, response PII, path traversal, and canary injection.';
994
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Security & Protocol Audit (14 Checks)';
995
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Non-destructive 14-point audit evaluating headers, CORS, secrets, PII, path traversal, canary injection, host headers, IP spoofing, HPP, cache-control, and metadata.';
630
996
  if (testTargetProfile) {
631
997
  testTargetProfile.innerHTML = `
632
998
  <option value="secure">Target: Secure API (Grade A)</option>
633
999
  <option value="vulnerable">Target: Vulnerable API (Grade F)</option>
634
1000
  `;
635
1001
  }
636
- if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run Security Audit';
1002
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 14-Point Security Audit';
637
1003
  scoreTitle.textContent = 'Ready to audit security';
638
- scoreSubtitle.textContent = 'Click "Run Security Audit" to evaluate all 7 defensive security, CORS, PII, and injection probes.';
1004
+ scoreSubtitle.textContent = 'Click "Run 14-Point Security Audit" to evaluate all 14 defensive security, CORS, PII, injection, and auth probes.';
639
1005
  } else {
640
1006
  if (tabResilience) tabResilience.classList.add('active');
641
- if (diagHeaderTitle) diagHeaderTitle.textContent = 'Automated API Resilience Benchmark';
642
- if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Automated 5-point test suite evaluating client resilience against network delay, throttling, cuts, corruptions, and 503 outages.';
1007
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Automated API Resilience Benchmark (10 Checks)';
1008
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Automated 10-point test suite evaluating client resilience against network delay, throttling, cuts, corruptions, 503 outages, jitter, circuit breakers, and starvation.';
643
1009
  if (testTargetProfile) {
644
1010
  testTargetProfile.innerHTML = `
645
1011
  <option value="resilient">Target: Resilient App (Grade A)</option>
646
1012
  <option value="fragile">Target: Fragile App (Grade F)</option>
647
1013
  `;
648
1014
  }
649
- if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 5-Point Benchmark';
1015
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 10-Point Resilience Benchmark';
650
1016
  scoreTitle.textContent = 'Ready to evaluate resilience';
651
- scoreSubtitle.textContent = 'Click "Run 5-Point Benchmark" to evaluate client resilience against 5 real failure cases.';
1017
+ scoreSubtitle.textContent = 'Click "Run 10-Point Resilience Benchmark" to evaluate client resilience against 10 real network failure cases.';
652
1018
  }
653
1019
  scoreValue.textContent = '--';
654
1020
  scoreGrade.textContent = 'UNTESTED';
@@ -658,6 +1024,7 @@ document.addEventListener('DOMContentLoaded', () => {
658
1024
  if (tabResilience) tabResilience.addEventListener('click', () => setDiagMode('resilience'));
659
1025
  if (tabSecurity) tabSecurity.addEventListener('click', () => setDiagMode('security'));
660
1026
  if (tabStorm) tabStorm.addEventListener('click', () => setDiagMode('storm'));
1027
+ if (tabRedTeam) tabRedTeam.addEventListener('click', () => setDiagMode('redteam'));
661
1028
 
662
1029
  if (testTargetProfile) {
663
1030
  testTargetProfile.addEventListener('change', () => {
@@ -670,95 +1037,497 @@ document.addEventListener('DOMContentLoaded', () => {
670
1037
  });
671
1038
  }
672
1039
 
673
- btnRunDiagnostics.addEventListener('click', async () => {
674
- if (isRunningDiagnostics) return;
675
- isRunningDiagnostics = true;
676
- btnRunDiagnostics.disabled = true;
1040
+ if (btnRunDiagnostics) {
1041
+ btnRunDiagnostics.addEventListener('click', async () => {
1042
+ if (isRunningDiagnostics) return;
1043
+ await syncTargetUrl();
1044
+ isRunningDiagnostics = true;
1045
+ btnRunDiagnostics.disabled = true;
1046
+
1047
+ if (currentDiagMode === 'storm') {
1048
+ btnRunDiagnostics.textContent = 'Auditing...';
1049
+ scoreTitle.textContent = 'Running Traffic Storm Benchmark...';
1050
+ scoreSubtitle.textContent = 'Auditing HTTP 429 rate limit backoff, oversized payloads (413), slowloris drips, and duplicate POST idempotency.';
1051
+
1052
+ const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
1053
+
1054
+ try {
1055
+ const res = await fetch(`/_faultmesh/storm/run?profile=${encodeURIComponent(profile)}`, {
1056
+ method: 'POST',
1057
+ headers: { 'Content-Type': 'application/json' },
1058
+ body: JSON.stringify({ profile }),
1059
+ });
1060
+ const report = await res.json();
1061
+ renderTrafficStormReport(report);
1062
+ refreshStatus();
1063
+ } catch (err) {
1064
+ console.error('Traffic storm error:', err);
1065
+ scoreTitle.textContent = 'Traffic Storm Audit Failed';
1066
+ scoreSubtitle.textContent = err.message;
1067
+ } finally {
1068
+ isRunningDiagnostics = false;
1069
+ btnRunDiagnostics.disabled = false;
1070
+ btnRunDiagnostics.textContent = 'Run 8-Point Storm Benchmark';
1071
+ }
1072
+ } else if (currentDiagMode === 'security') {
1073
+ btnRunDiagnostics.textContent = 'Auditing...';
1074
+ scoreTitle.textContent = 'Running 14-Point Security Audit...';
1075
+ scoreSubtitle.textContent = 'Auditing defensive headers, CORS safety, query secret leakage, response PII, and inert canary probes.';
1076
+
1077
+ const profile = testTargetProfile ? testTargetProfile.value : 'secure';
1078
+
1079
+ try {
1080
+ const res = await fetch(`/_faultmesh/security/run?profile=${encodeURIComponent(profile)}`, {
1081
+ method: 'POST',
1082
+ headers: { 'Content-Type': 'application/json' },
1083
+ body: JSON.stringify({ profile }),
1084
+ });
1085
+ const report = await res.json();
1086
+ renderSecurityReport(report);
1087
+ refreshStatus();
1088
+ } catch (err) {
1089
+ console.error('Security audit error:', err);
1090
+ scoreTitle.textContent = 'Security Audit Failed';
1091
+ scoreSubtitle.textContent = err.message;
1092
+ } finally {
1093
+ isRunningDiagnostics = false;
1094
+ btnRunDiagnostics.disabled = false;
1095
+ btnRunDiagnostics.textContent = 'Run 14-Point Security Audit';
1096
+ }
1097
+ } else {
1098
+ btnRunDiagnostics.textContent = 'Running...';
1099
+ scoreTitle.textContent = 'Running 10-Point Resilience Benchmark...';
1100
+ scoreSubtitle.textContent = 'Evaluating delay, slow speed, cuts, corruption, 503 errors, jitter, circuit breakers, and starvation.';
1101
+
1102
+ const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
1103
+
1104
+ try {
1105
+ const res = await fetch(`/_faultmesh/diagnostics/run?profile=${encodeURIComponent(profile)}`, {
1106
+ method: 'POST',
1107
+ headers: { 'Content-Type': 'application/json' },
1108
+ body: JSON.stringify({ profile }),
1109
+ });
1110
+ const report = await res.json();
1111
+ renderDiagnosticsReport(report);
1112
+ refreshStatus();
1113
+ } catch (err) {
1114
+ console.error('Diagnostics error:', err);
1115
+ scoreTitle.textContent = 'Diagnostics Failed';
1116
+ scoreSubtitle.textContent = err.message;
1117
+ } finally {
1118
+ isRunningDiagnostics = false;
1119
+ btnRunDiagnostics.disabled = false;
1120
+ btnRunDiagnostics.textContent = 'Run 10-Point Resilience Benchmark';
1121
+ }
1122
+ }
1123
+ });
1124
+ }
1125
+
1126
+ // Remediation Code Snippets Catalog
1127
+ const REMEDIATION_SNIPPETS = {
1128
+ 'Response Delay Handling (300ms)': {
1129
+ express: `// Express / Node HTTP Client Timeout Handling
1130
+ const controller = new AbortController();
1131
+ const timeout = setTimeout(() => controller.abort(), 3000); // 3s SLA timeout
1132
+
1133
+ try {
1134
+ const res = await fetch('http://api.backend.internal/data', { signal: controller.signal });
1135
+ return await res.json();
1136
+ } catch (err) {
1137
+ if (err.name === 'AbortError') {
1138
+ return { status: 'degraded', data: [] }; // Graceful degradation fallback
1139
+ }
1140
+ throw err;
1141
+ } finally {
1142
+ clearTimeout(timeout);
1143
+ }`,
1144
+ fastapi: `# Python / httpx Client SLA Timeout
1145
+ import httpx
1146
+
1147
+ try:
1148
+ async with httpx.AsyncClient(timeout=3.0) as client:
1149
+ response = await client.get("http://api.backend.internal/data")
1150
+ return response.json()
1151
+ except httpx.TimeoutException:
1152
+ return {"status": "degraded", "data": []} # Graceful fallback`
1153
+ },
1154
+
1155
+ 'Bandwidth Throttling (Speed Cap)': {
1156
+ express: `// Streaming chunk processor with backpressure handling
1157
+ const res = await fetch(url);
1158
+ const reader = res.body.getReader();
1159
+
1160
+ while (true) {
1161
+ const { done, value } = await reader.read();
1162
+ if (done) break;
1163
+ processChunkProgressively(value);
1164
+ }`,
1165
+ fastapi: `# Streaming response chunk processor
1166
+ import httpx
1167
+
1168
+ async with httpx.AsyncClient() as client:
1169
+ async with client.stream("GET", url) as response:
1170
+ async for chunk in response.aiter_bytes():
1171
+ process_chunk_incrementally(chunk)`
1172
+ },
677
1173
 
678
- if (currentDiagMode === 'storm') {
679
- btnRunDiagnostics.textContent = 'Auditing...';
680
- scoreTitle.textContent = 'Running Traffic Storm Benchmark...';
681
- scoreSubtitle.textContent = 'Auditing HTTP 429 rate limit backoff, oversized payloads (413), slowloris drips, and duplicate POST idempotency.';
1174
+ 'Connection Cut (Abrupt Socket Drop)': {
1175
+ express: `// Retry with Exponential Backoff on Socket Drop / ECONNRESET
1176
+ async function fetchWithRetry(url, retries = 3, delay = 500) {
1177
+ for (let i = 0; i < retries; i++) {
1178
+ try {
1179
+ return await fetch(url);
1180
+ } catch (err) {
1181
+ if (i === retries - 1) throw err;
1182
+ await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
1183
+ }
1184
+ }
1185
+ }`,
1186
+ fastapi: `# Tenacity retry on connection errors
1187
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
1188
+ import httpx
1189
+
1190
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5), retry=retry_if_exception_type(httpx.NetworkError))
1191
+ async def safe_fetch(url: str):
1192
+ async with httpx.AsyncClient() as client:
1193
+ return await client.get(url)`
1194
+ },
682
1195
 
683
- const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
1196
+ 'Corrupted / Truncated JSON Payload': {
1197
+ express: `// Safe JSON Parser with Fallback Boundary
1198
+ let parsedData;
1199
+ try {
1200
+ parsedData = JSON.parse(responseText);
1201
+ } catch (err) {
1202
+ console.warn("Payload corrupted or truncated mid-stream. Triggering safe fallback.");
1203
+ parsedData = { fallback: true, error: "Malformed payload received" };
1204
+ }`,
1205
+ fastapi: `# Pydantic validation error boundary
1206
+ import json
1207
+ from pydantic import ValidationError
1208
+
1209
+ try:
1210
+ data = json.loads(response_text)
1211
+ validated = MyResponseModel.model_validate(data)
1212
+ except (json.JSONDecodeError, ValidationError):
1213
+ validated = MyResponseModel(fallback=True, items=[])`
1214
+ },
684
1215
 
685
- try {
686
- const res = await fetch(`/_faultmesh/storm/run?profile=${encodeURIComponent(profile)}`, {
687
- method: 'POST',
688
- headers: { 'Content-Type': 'application/json' },
689
- body: JSON.stringify({ profile }),
690
- });
691
- const report = await res.json();
692
- renderTrafficStormReport(report);
693
- refreshStatus();
694
- } catch (err) {
695
- console.error('Traffic storm error:', err);
696
- scoreTitle.textContent = 'Traffic Storm Audit Failed';
697
- scoreSubtitle.textContent = err.message;
698
- } finally {
699
- isRunningDiagnostics = false;
700
- btnRunDiagnostics.disabled = false;
701
- btnRunDiagnostics.textContent = 'Run Traffic Storm Benchmark';
702
- }
703
- } else if (currentDiagMode === 'security') {
704
- btnRunDiagnostics.textContent = 'Auditing...';
705
- scoreTitle.textContent = 'Running Security Audit...';
706
- scoreSubtitle.textContent = 'Auditing defensive headers, CORS safety, query secret leakage, response PII, and inert canary probes.';
1216
+ 'Server Outage (503 Service Unavailable)': {
1217
+ express: `// 503 Circuit Breaker & Status Fallback
1218
+ if (res.status === 503) {
1219
+ const retryAfter = res.headers.get('Retry-After') || '5';
1220
+ console.info(\`Upstream busy. Backing off for \${retryAfter} seconds.\`);
1221
+ return { degraded: true, message: "Service busy, please retry shortly" };
1222
+ }`,
1223
+ fastapi: `# 503 Status Handler with Retry-After respect
1224
+ if response.status_code == 503:
1225
+ retry_after = int(response.headers.get("Retry-After", 5))
1226
+ logger.warning(f"Upstream service unavailable. Backing off for {retry_after}s.")
1227
+ return {"degraded": True, "message": "Service unavailable"}`
1228
+ },
707
1229
 
708
- const profile = testTargetProfile ? testTargetProfile.value : 'secure';
1230
+ 'Defensive Security Headers': {
1231
+ express: `// Add Helmet.js to automatically inject defensive headers
1232
+ import helmet from 'helmet';
1233
+ app.use(helmet());
1234
+
1235
+ // Or configure manually in Express:
1236
+ app.use((req, res, next) => {
1237
+ res.setHeader('X-Content-Type-Options', 'nosniff');
1238
+ res.setHeader('X-Frame-Options', 'DENY');
1239
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
1240
+ next();
1241
+ });`,
1242
+ fastapi: `# Custom Security Headers Middleware in FastAPI
1243
+ from starlette.middleware.base import BaseHTTPMiddleware
1244
+
1245
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
1246
+ async def dispatch(self, request, call_next):
1247
+ response = await call_next(request)
1248
+ response.headers["X-Content-Type-Options"] = "nosniff"
1249
+ response.headers["X-Frame-Options"] = "DENY"
1250
+ response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
1251
+ return response
1252
+
1253
+ app.add_middleware(SecurityHeadersMiddleware)`
1254
+ },
709
1255
 
710
- try {
711
- const res = await fetch(`/_faultmesh/security/run?profile=${encodeURIComponent(profile)}`, {
712
- method: 'POST',
713
- headers: { 'Content-Type': 'application/json' },
714
- body: JSON.stringify({ profile }),
715
- });
716
- const report = await res.json();
717
- renderSecurityReport(report);
718
- refreshStatus();
719
- } catch (err) {
720
- console.error('Security audit error:', err);
721
- scoreTitle.textContent = 'Security Audit Failed';
722
- scoreSubtitle.textContent = err.message;
723
- } finally {
724
- isRunningDiagnostics = false;
725
- btnRunDiagnostics.disabled = false;
726
- btnRunDiagnostics.textContent = 'Run Security Audit';
727
- }
728
- } else {
729
- btnRunDiagnostics.textContent = 'Running...';
730
- scoreTitle.textContent = 'Running 5-Point Benchmark...';
731
- scoreSubtitle.textContent = 'Evaluating delay, slow speed, connection drops, corrupted JSON, and 503 errors.';
1256
+ 'CORS Origin & Credential Safety': {
1257
+ express: `// Strict Whitelist CORS (Never use origin: '*' with credentials)
1258
+ import cors from 'cors';
1259
+
1260
+ const allowedOrigins = ['https://app.yourdomain.com'];
1261
+ app.use(cors({
1262
+ origin: (origin, callback) => {
1263
+ if (!origin || allowedOrigins.includes(origin)) callback(null, true);
1264
+ else callback(new Error('Blocked by CORS'));
1265
+ },
1266
+ credentials: true
1267
+ }));`,
1268
+ fastapi: `# Strict CORS Middleware in FastAPI
1269
+ from fastapi.middleware.cors import CORSMiddleware
1270
+
1271
+ app.add_middleware(
1272
+ CORSMiddleware,
1273
+ allow_origins=["https://app.yourdomain.com"], # Explicit origin required
1274
+ allow_credentials=True,
1275
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
1276
+ allow_headers=["Authorization", "Content-Type"],
1277
+ )`
1278
+ },
732
1279
 
733
- const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
1280
+ 'URL Credential & Secret Exposure': {
1281
+ express: `// Consume tokens via Authorization header, reject query tokens
1282
+ app.use((req, res, next) => {
1283
+ if (req.query.token || req.query.apiKey) {
1284
+ return res.status(400).json({ error: "Pass credentials in Authorization header, not in query string." });
1285
+ }
1286
+ next();
1287
+ });`,
1288
+ fastapi: `# OAuth2 / Bearer Token Header Authentication
1289
+ from fastapi import Depends, HTTPException, Security
1290
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
734
1291
 
735
- try {
736
- const res = await fetch(`/_faultmesh/diagnostics/run?profile=${encodeURIComponent(profile)}`, {
737
- method: 'POST',
738
- headers: { 'Content-Type': 'application/json' },
739
- body: JSON.stringify({ profile }),
740
- });
741
- const report = await res.json();
742
- renderDiagnosticsReport(report);
743
- refreshStatus();
744
- } catch (err) {
745
- console.error('Diagnostics error:', err);
746
- scoreTitle.textContent = 'Diagnostics Failed';
747
- scoreSubtitle.textContent = err.message;
748
- } finally {
749
- isRunningDiagnostics = false;
750
- btnRunDiagnostics.disabled = false;
751
- btnRunDiagnostics.textContent = 'Run 5-Point Benchmark';
752
- }
753
- }
1292
+ security = HTTPBearer()
1293
+
1294
+ async def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
1295
+ token = credentials.credentials
1296
+ return verify_token(token)`
1297
+ },
1298
+
1299
+ 'Outbound Response PII & Secret Scanner': {
1300
+ express: `// Exclude passwords and internal keys before serialization
1301
+ function sanitizeUser(user) {
1302
+ const { passwordHash, internalSecret, ...safeUser } = user;
1303
+ return safeUser;
1304
+ }
1305
+
1306
+ res.json(sanitizeUser(userRecord));`,
1307
+ fastapi: `# Use Pydantic response_model to prevent leaking private fields
1308
+ class UserPublic(BaseModel):
1309
+ id: int
1310
+ username: str
1311
+ email: EmailStr
1312
+ # password_hash and internal_token are excluded
1313
+
1314
+ @app.get("/users/me", response_model=UserPublic)
1315
+ async def read_current_user():
1316
+ return user_record`
1317
+ },
1318
+
1319
+ 'Error Sanitization & Stack Trace Exposure': {
1320
+ express: `// Global Production Error Sanitizer (Hide stack traces from clients)
1321
+ app.use((err, req, res, next) => {
1322
+ console.error(err); // Log internally to server stdout
1323
+ res.status(500).json({
1324
+ error: "Internal Server Error",
1325
+ requestId: req.id
754
1326
  });
1327
+ });`,
1328
+ fastapi: `# Production Exception Handler
1329
+ from fastapi import Request
1330
+ from fastapi.responses import JSONResponse
1331
+
1332
+ @app.exception_handler(Exception)
1333
+ async def global_exception_handler(request: Request, exc: Exception):
1334
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
1335
+ return JSONResponse(
1336
+ status_code=500,
1337
+ content={"error": "Internal Server Error", "requestId": request.state.request_id}
1338
+ )`
1339
+ },
1340
+
1341
+ 'Path Traversal & Directory Escape (../)': {
1342
+ express: `// Safe Path Resolution with Boundary Jailing
1343
+ import path from 'node:path';
1344
+
1345
+ function getSafeFile(userPath) {
1346
+ const baseDir = path.resolve('/var/www/uploads');
1347
+ const safePath = path.resolve(baseDir, userPath);
1348
+ if (!safePath.startsWith(baseDir + path.sep)) {
1349
+ throw new Error('Access Denied: Path Traversal Detected');
1350
+ }
1351
+ return safePath;
1352
+ }`,
1353
+ fastapi: `# Safe Path Resolution in Python
1354
+ from pathlib import Path
1355
+
1356
+ def get_safe_filepath(user_filename: str) -> Path:
1357
+ base_dir = Path("/var/www/uploads").resolve()
1358
+ target_path = (base_dir / user_filename).resolve()
1359
+ if not target_path.is_relative_to(base_dir):
1360
+ raise HTTPException(status_code=400, detail="Invalid path")
1361
+ return target_path`
1362
+ },
1363
+
1364
+ 'Safe SQL/NoSQL & Canary Input Probing': {
1365
+ express: `// Use Parameterized Queries (e.g. pg, mysql2, or Prisma ORM)
1366
+ // NEVER do: db.query(\`SELECT * FROM users WHERE id = '\${userId}'\`)
1367
+
1368
+ // ALWAYS do:
1369
+ const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);`,
1370
+ fastapi: `# Use SQLAlchemy / SQLModel Parameterized Statements
1371
+ from sqlalchemy import select
1372
+
1373
+ # Safe parameterized query:
1374
+ stmt = select(User).where(User.username == username_input)
1375
+ result = await session.execute(stmt)`
1376
+ },
1377
+
1378
+ 'Rate Limit Back-off & Retry Storm Handling': {
1379
+ express: `// Express Rate Limit Middleware
1380
+ import rateLimit from 'express-rate-limit';
1381
+
1382
+ const limiter = rateLimit({
1383
+ windowMs: 60 * 1000, // 1 minute
1384
+ max: 60, // Limit each IP to 60 requests per minute
1385
+ standardHeaders: true, // Return RateLimit-* headers
1386
+ legacyHeaders: false,
1387
+ });
1388
+
1389
+ app.use('/api/', limiter);`,
1390
+ fastapi: `# SlowAPI Rate Limiting for FastAPI
1391
+ from slowapi import Limiter, _rate_limit_exceeded_handler
1392
+ from slowapi.util import get_remote_address
1393
+ from slowapi.errors import RateLimitExceeded
1394
+
1395
+ limiter = Limiter(key_func=get_remote_address)
1396
+ app.state.limiter = limiter
1397
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
1398
+
1399
+ @app.get("/api/data")
1400
+ @limiter.limit("60/minute")
1401
+ async def get_data(request: Request):
1402
+ return {"status": "ok"}`
1403
+ },
1404
+
1405
+ 'Oversized Payload & Buffer OOM Defense (HTTP 413)': {
1406
+ express: `// Restrict JSON Body Parsing Limit
1407
+ app.use(express.json({ limit: '2mb' }));
1408
+ app.use(express.urlencoded({ extended: true, limit: '2mb' }));`,
1409
+ fastapi: `# Request Body Size Limiter Middleware in FastAPI
1410
+ from starlette.middleware.base import BaseHTTPMiddleware
1411
+ from starlette.responses import JSONResponse
1412
+
1413
+ class ContentLengthLimitMiddleware(BaseHTTPMiddleware):
1414
+ async def dispatch(self, request, call_next):
1415
+ content_length = request.headers.get("content-length")
1416
+ if content_length and int(content_length) > 5 * 1024 * 1024: # 5 MB
1417
+ return JSONResponse({"error": "Payload Too Large"}, status_code=413)
1418
+ return await call_next(request)
1419
+
1420
+ app.add_middleware(ContentLengthLimitMiddleware)`
1421
+ },
1422
+
1423
+ 'Slowloris Connection Drip Defense': {
1424
+ express: `// Node HTTP Server Timeout Protection
1425
+ const server = app.listen(port);
1426
+
1427
+ server.headersTimeout = 20000; // 20s
1428
+ server.requestTimeout = 30000; // 30s
1429
+ server.keepAliveTimeout = 5000; // 5s`,
1430
+ fastapi: `# Uvicorn Timeout Configuration
1431
+ # Run uvicorn with strict timeout flags:
1432
+ # uvicorn main:app --timeout-keep-alive 5 --timeout-graceful-shutdown 30`
1433
+ },
1434
+
1435
+ 'Duplicate Request Idempotency Protection': {
1436
+ express: `// Idempotency Middleware using Redis / In-Memory Cache
1437
+ app.post('/api/checkout', async (req, res) => {
1438
+ const key = req.headers['idempotency-key'];
1439
+ if (!key) return res.status(400).json({ error: "Missing Idempotency-Key" });
1440
+
1441
+ const cached = await redis.get(\`idemp:\${key}\`);
1442
+ if (cached) return res.json(JSON.parse(cached));
1443
+
1444
+ const result = await processTransaction(req.body);
1445
+ await redis.set(\`idemp:\${key}\`, JSON.stringify(result), 'EX', 86400);
1446
+ res.json(result);
1447
+ });`,
1448
+ fastapi: `# Idempotency Key Middleware
1449
+ from fastapi import Header, HTTPException
1450
+
1451
+ @app.post("/api/checkout")
1452
+ async def checkout(idempotency_key: str = Header(None)):
1453
+ if not idempotency_key:
1454
+ raise HTTPException(status_code=400, detail="Missing Idempotency-Key")
1455
+
1456
+ cached = await redis_client.get(f"idemp:{idempotency_key}")
1457
+ if cached:
1458
+ return json.loads(cached)
1459
+
1460
+ result = await process_order()
1461
+ await redis_client.set(f"idemp:{idempotency_key}", json.dumps(result), ex=86400)
1462
+ return result`
1463
+ }
1464
+ };
1465
+
1466
+ function renderSnippetBlock(checkName, uniqueId) {
1467
+ const snippet = REMEDIATION_SNIPPETS[checkName];
1468
+ if (!snippet) return '';
1469
+
1470
+ return `
1471
+ <button class="btn-toggle-snippet" type="button" onclick="window.toggleSnippet('${uniqueId}')">
1472
+ View Remediation Snippet
1473
+ </button>
1474
+ <div id="${uniqueId}" class="snippet-container" style="display: none;">
1475
+ <div class="snippet-header">
1476
+ <div class="snippet-tabs">
1477
+ <button class="snippet-tab-btn active" type="button" onclick="window.switchSnippetTab('${uniqueId}', 'express')">Express / Node</button>
1478
+ <button class="snippet-tab-btn" type="button" onclick="window.switchSnippetTab('${uniqueId}', 'fastapi')">FastAPI / Python</button>
1479
+ </div>
1480
+ <button class="btn-copy-snippet" type="button" onclick="window.copySnippetCode('${uniqueId}', this)">Copy Code</button>
1481
+ </div>
1482
+ <pre class="snippet-code-content" data-express="${escapeHtml(snippet.express)}" data-fastapi="${escapeHtml(snippet.fastapi)}">${escapeHtml(snippet.express)}</pre>
1483
+ </div>
1484
+ `;
1485
+ }
1486
+
1487
+ window.toggleSnippet = function(id) {
1488
+ const el = document.getElementById(id);
1489
+ if (!el) return;
1490
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
1491
+ };
1492
+
1493
+ window.switchSnippetTab = function(containerId, lang) {
1494
+ const container = document.getElementById(containerId);
1495
+ if (!container) return;
1496
+ const pre = container.querySelector('.snippet-code-content');
1497
+ const tabs = container.querySelectorAll('.snippet-tab-btn');
1498
+ tabs.forEach(t => t.classList.remove('active'));
1499
+
1500
+ const clicked = Array.from(tabs).find(t => t.textContent.toLowerCase().includes(lang === 'express' ? 'express' : 'fastapi'));
1501
+ if (clicked) clicked.classList.add('active');
1502
+
1503
+ if (pre) {
1504
+ pre.textContent = pre.getAttribute(`data-${lang}`) || '';
1505
+ }
1506
+ };
1507
+
1508
+ window.copySnippetCode = function(containerId, btnEl) {
1509
+ const container = document.getElementById(containerId);
1510
+ if (!container) return;
1511
+ const pre = container.querySelector('.snippet-code-content');
1512
+ if (!pre) return;
1513
+ navigator.clipboard.writeText(pre.textContent).then(() => {
1514
+ if (btnEl) {
1515
+ const orig = btnEl.textContent;
1516
+ btnEl.textContent = 'Copied!';
1517
+ setTimeout(() => { btnEl.textContent = orig; }, 1500);
1518
+ }
1519
+ });
1520
+ };
755
1521
 
756
1522
  function renderDiagnosticsReport(report) {
1523
+ lastScorecardData = { mode: 'resilience', report, timestamp: new Date().toISOString() };
1524
+ if (exportGroup) exportGroup.style.display = 'flex';
1525
+
757
1526
  scoreValue.textContent = `${report.score}/100`;
758
1527
  scoreGrade.textContent = `GRADE ${report.grade}`;
759
1528
 
760
1529
  if (report.grade === 'A') {
761
- scoreTitle.textContent = 'All 5 Resilience Checks Passed (Grade A)';
1530
+ scoreTitle.textContent = 'All 10 Resilience Checks Passed (Grade A)';
762
1531
  } else if (report.grade === 'B') {
763
1532
  scoreTitle.textContent = 'Passed with Minor Issues (Grade B)';
764
1533
  } else {
@@ -777,6 +1546,7 @@ document.addEventListener('DOMContentLoaded', () => {
777
1546
  const num = String(idx + 1).padStart(2, '0');
778
1547
  const rowClass = r.passed ? 'row-passed' : 'row-failed';
779
1548
  const findingClass = r.passed ? 'finding-pass' : 'finding-fail';
1549
+ const snippetHtml = !r.passed ? renderSnippetBlock(r.name, `snip_res_${idx}`) : '';
780
1550
 
781
1551
  return `
782
1552
  <div class="test-row ${rowClass}">
@@ -791,6 +1561,7 @@ document.addEventListener('DOMContentLoaded', () => {
791
1561
  <div class="test-finding-box ${findingClass}">
792
1562
  Finding: ${escapeHtml(r.details)}
793
1563
  </div>
1564
+ ${snippetHtml}
794
1565
  </div>
795
1566
  </div>
796
1567
  <div class="test-row-aside">
@@ -812,11 +1583,14 @@ document.addEventListener('DOMContentLoaded', () => {
812
1583
  }
813
1584
 
814
1585
  function renderSecurityReport(report) {
1586
+ lastScorecardData = { mode: 'security', report, timestamp: new Date().toISOString() };
1587
+ if (exportGroup) exportGroup.style.display = 'flex';
1588
+
815
1589
  scoreValue.textContent = `${report.score}/100`;
816
1590
  scoreGrade.textContent = `GRADE ${report.grade}`;
817
1591
 
818
1592
  if (report.grade === 'A') {
819
- scoreTitle.textContent = 'All 7 Security Checks Passed (Grade A)';
1593
+ scoreTitle.textContent = 'All 14 Security Checks Passed (Grade A)';
820
1594
  } else if (report.grade === 'B') {
821
1595
  scoreTitle.textContent = 'Minor Security Issues Detected (Grade B)';
822
1596
  } else {
@@ -834,6 +1608,7 @@ document.addEventListener('DOMContentLoaded', () => {
834
1608
  const num = String(idx + 1).padStart(2, '0');
835
1609
  const rowClass = c.passed ? 'row-passed' : 'row-failed';
836
1610
  const findingClass = c.passed ? 'finding-pass' : 'finding-fail';
1611
+ const snippetHtml = !c.passed ? renderSnippetBlock(c.name, `snip_sec_${idx}`) : '';
837
1612
 
838
1613
  return `
839
1614
  <div class="test-row ${rowClass}">
@@ -849,6 +1624,7 @@ document.addEventListener('DOMContentLoaded', () => {
849
1624
  <div class="test-finding-box ${findingClass}">
850
1625
  Finding: ${escapeHtml(c.details)}
851
1626
  </div>
1627
+ ${snippetHtml}
852
1628
  </div>
853
1629
  </div>
854
1630
  <div class="test-row-aside">
@@ -870,11 +1646,14 @@ document.addEventListener('DOMContentLoaded', () => {
870
1646
  }
871
1647
 
872
1648
  function renderTrafficStormReport(report) {
1649
+ lastScorecardData = { mode: 'storm', report, timestamp: new Date().toISOString() };
1650
+ if (exportGroup) exportGroup.style.display = 'flex';
1651
+
873
1652
  scoreValue.textContent = `${report.score}/100`;
874
1653
  scoreGrade.textContent = `GRADE ${report.grade}`;
875
1654
 
876
1655
  if (report.grade === 'A') {
877
- scoreTitle.textContent = 'All 4 Traffic Storm Checks Passed (Grade A)';
1656
+ scoreTitle.textContent = 'All 8 Traffic Storm Checks Passed (Grade A)';
878
1657
  } else if (report.grade === 'B') {
879
1658
  scoreTitle.textContent = 'Passed with Minor Issues (Grade B)';
880
1659
  } else {
@@ -892,6 +1671,7 @@ document.addEventListener('DOMContentLoaded', () => {
892
1671
  const num = String(idx + 1).padStart(2, '0');
893
1672
  const rowClass = c.passed ? 'row-passed' : 'row-failed';
894
1673
  const findingClass = c.passed ? 'finding-pass' : 'finding-fail';
1674
+ const snippetHtml = !c.passed ? renderSnippetBlock(c.name, `snip_storm_${idx}`) : '';
895
1675
 
896
1676
  return `
897
1677
  <div class="test-row ${rowClass}">
@@ -907,6 +1687,7 @@ document.addEventListener('DOMContentLoaded', () => {
907
1687
  <div class="test-finding-box ${findingClass}">
908
1688
  Finding: ${escapeHtml(c.details)}
909
1689
  </div>
1690
+ ${snippetHtml}
910
1691
  </div>
911
1692
  </div>
912
1693
  <div class="test-row-aside">
@@ -927,45 +1708,588 @@ document.addEventListener('DOMContentLoaded', () => {
927
1708
  `;
928
1709
  }
929
1710
 
930
- // 9. Live Request SSE Stream
931
- function setupSSE() {
932
- const eventSource = new EventSource('/_faultmesh/telemetry/stream');
933
-
934
- eventSource.addEventListener('snapshot', (e) => {
935
- try {
936
- const data = JSON.parse(e.data);
937
- if (data.events && data.events.length > 0) {
938
- activityFeed.innerHTML = '';
939
- data.events.forEach(addLogRow);
940
- }
941
- } catch (err) {
942
- console.warn('Snapshot parse error', err);
943
- }
1711
+ // Export functions
1712
+ function exportMarkdownReport() {
1713
+ if (!lastScorecardData || !lastScorecardData.report) {
1714
+ alert('No audit report available to export. Run a benchmark first.');
1715
+ return;
1716
+ }
1717
+ const { mode, report, timestamp } = lastScorecardData;
1718
+ const checks = report.checks || report.results || [];
1719
+
1720
+ let md = `# FaultMesh Audit Report — ${mode.toUpperCase()}\n\n`;
1721
+ md += `* **Timestamp:** ${new Date(timestamp).toUTCString()}\n`;
1722
+ md += `* **Overall Score:** ${report.score} / 100 (Grade ${report.grade})\n`;
1723
+ md += `* **Checks Passed:** ${report.passedChecks ?? report.passedAttacks} / ${report.totalChecks ?? report.totalAttacks}\n\n`;
1724
+
1725
+ md += `## Findings & Verification Summary\n\n`;
1726
+ md += `| # | Check Name | Severity | Result | Finding Details |\n`;
1727
+ md += `| :--- | :--- | :--- | :--- | :--- |\n`;
1728
+ checks.forEach((c, idx) => {
1729
+ const num = String(idx + 1).padStart(2, '0');
1730
+ const sev = c.severity ? c.severity.toUpperCase() : 'STANDARD';
1731
+ const res = c.passed ? 'PASSED' : 'FAILED';
1732
+ const detail = (c.details || '').replace(/\|/g, '\\|');
1733
+ md += `| ${num} | ${c.name} | ${sev} | ${res} | ${detail} |\n`;
944
1734
  });
945
1735
 
946
- eventSource.addEventListener('telemetry', (e) => {
947
- try {
948
- const event = JSON.parse(e.data);
949
- addLogRow(event);
950
- refreshStatus();
951
- } catch (err) {
952
- console.warn('Telemetry parse error', err);
953
- }
954
- });
1736
+ if (report.recommendations && report.recommendations.length > 0) {
1737
+ md += `\n## Remediation Recommendations\n\n`;
1738
+ report.recommendations.forEach(rec => {
1739
+ md += `* ${rec}\n`;
1740
+ });
1741
+ }
1742
+
1743
+ md += `\n---\n*Report generated by FaultMesh Runtime Engine*\n`;
1744
+
1745
+ downloadFile(`faultmesh-${mode}-audit-${Date.now()}.md`, 'text/markdown', md);
955
1746
  }
956
1747
 
957
- function addLogRow(ev) {
958
- if (activityFeed.querySelector('.table-empty')) {
959
- activityFeed.innerHTML = '';
1748
+ function exportJsonReport() {
1749
+ if (!lastScorecardData || !lastScorecardData.report) {
1750
+ alert('No audit report available to export. Run a benchmark first.');
1751
+ return;
960
1752
  }
1753
+ const jsonStr = JSON.stringify(lastScorecardData, null, 2);
1754
+ downloadFile(`faultmesh-${lastScorecardData.mode}-audit-${Date.now()}.json`, 'application/json', jsonStr);
1755
+ }
961
1756
 
962
- const tr = document.createElement('tr');
963
- let codeClass = 'code-2xx';
964
- if (ev.statusCode >= 500) codeClass = 'code-5xx';
965
- else if (ev.statusCode >= 400) codeClass = 'code-4xx';
1757
+ function downloadFile(filename, mimeType, content) {
1758
+ const blob = new Blob([content], { type: mimeType });
1759
+ const url = URL.createObjectURL(blob);
1760
+ const a = document.createElement('a');
1761
+ a.href = url;
1762
+ a.download = filename;
1763
+ document.body.appendChild(a);
1764
+ a.click();
1765
+ document.body.removeChild(a);
1766
+ URL.revokeObjectURL(url);
1767
+ }
966
1768
 
967
- const ruleText = ev.appliedToxics && ev.appliedToxics.length > 0
968
- ? ev.appliedToxics.join(', ')
1769
+ if (btnExportMd) {
1770
+ btnExportMd.addEventListener('click', exportMarkdownReport);
1771
+ }
1772
+ if (btnExportJson) {
1773
+ btnExportJson.addEventListener('click', exportJsonReport);
1774
+ }
1775
+
1776
+ // 8.9 Auto-Healer Interactions
1777
+ if (btnOpenHealer && healerModal) {
1778
+ btnOpenHealer.addEventListener('click', () => {
1779
+ if (healerProjectDir && (!healerProjectDir.value || healerProjectDir.value === '.' || healerProjectDir.value === './')) {
1780
+ healerProjectDir.value = 'examples/vulnerable-backend';
1781
+ }
1782
+ healerModal.style.display = 'flex';
1783
+ if (healerScanStatus && healerScanStatus.style.display === 'none') {
1784
+ healerScanStatus.style.display = 'block';
1785
+ healerScanStatus.textContent = 'Enter your backend project directory above and click "Scan Codebase".';
1786
+ }
1787
+ });
1788
+ }
1789
+
1790
+ if (btnCloseHealer && healerModal) {
1791
+ btnCloseHealer.addEventListener('click', () => {
1792
+ healerModal.style.display = 'none';
1793
+ });
1794
+ }
1795
+
1796
+ // Close modal on backdrop click
1797
+ if (healerModal) {
1798
+ healerModal.addEventListener('click', (e) => {
1799
+ if (e.target === healerModal) {
1800
+ healerModal.style.display = 'none';
1801
+ }
1802
+ });
1803
+ }
1804
+
1805
+ const btnDirPresetSample = document.getElementById('btnDirPresetSample');
1806
+ const btnDirPresetRoot = document.getElementById('btnDirPresetRoot');
1807
+ if (btnDirPresetSample && healerProjectDir) {
1808
+ btnDirPresetSample.addEventListener('click', () => {
1809
+ healerProjectDir.value = 'examples/vulnerable-backend';
1810
+ if (btnHealerScan) btnHealerScan.click();
1811
+ });
1812
+ }
1813
+ if (btnDirPresetRoot && healerProjectDir) {
1814
+ btnDirPresetRoot.addEventListener('click', () => {
1815
+ healerProjectDir.value = './';
1816
+ healerProjectDir.focus();
1817
+ });
1818
+ }
1819
+
1820
+ // Target URL Management (Header Switchers)
1821
+ const btnTarget5050 = document.getElementById('btnTarget5050');
1822
+ const btnTarget4000 = document.getElementById('btnTarget4000');
1823
+
1824
+ async function applyTargetUrl(url) {
1825
+ if (!url) return;
1826
+ try {
1827
+ const res = await fetch('/_faultmesh/config/target', {
1828
+ method: 'POST',
1829
+ headers: { 'Content-Type': 'application/json' },
1830
+ body: JSON.stringify({ targetUrl: url }),
1831
+ });
1832
+ const data = await res.json();
1833
+ if (data.success) {
1834
+ if (targetUrlInput) targetUrlInput.value = url;
1835
+ if (testTargetProfile) {
1836
+ testTargetProfile.textContent = url.includes(':5050') ? 'Sample Backend (:5050)' : (url.includes(':4000') ? 'Mock Echo (:4000)' : url);
1837
+ }
1838
+ }
1839
+ } catch (err) {
1840
+ console.warn('Failed to update target URL:', err);
1841
+ }
1842
+ }
1843
+
1844
+ if (btnSetTargetUrl && targetUrlInput) {
1845
+ btnSetTargetUrl.addEventListener('click', () => {
1846
+ applyTargetUrl(targetUrlInput.value.trim());
1847
+ });
1848
+ }
1849
+ if (btnTarget5050) {
1850
+ btnTarget5050.addEventListener('click', () => {
1851
+ applyTargetUrl('http://127.0.0.1:5050');
1852
+ });
1853
+ }
1854
+ if (btnTarget4000) {
1855
+ btnTarget4000.addEventListener('click', () => {
1856
+ applyTargetUrl('http://127.0.0.1:4000');
1857
+ });
1858
+ }
1859
+
1860
+ if (btnToggleAiSettings && healerAiSettingsPanel) {
1861
+ btnToggleAiSettings.addEventListener('click', () => {
1862
+ const isHidden = healerAiSettingsPanel.style.display === 'none';
1863
+ healerAiSettingsPanel.style.display = isHidden ? 'block' : 'none';
1864
+ btnToggleAiSettings.textContent = isHidden
1865
+ ? 'Hide AI Healer Configuration'
1866
+ : 'Configure AI Healer (Local LLM / Cloud AI Providers)';
1867
+ });
1868
+ }
1869
+
1870
+ if (healerAiProvider) {
1871
+ healerAiProvider.addEventListener('change', () => {
1872
+ const prov = healerAiProvider.value;
1873
+ if (prov === 'ollama') {
1874
+ if (aiKeyGroup) aiKeyGroup.style.display = 'none';
1875
+ if (aiEndpointGroup) {
1876
+ aiEndpointGroup.style.display = 'flex';
1877
+ if (healerAiEndpoint) healerAiEndpoint.value = 'http://127.0.0.1:11434';
1878
+ }
1879
+ } else if (prov === 'custom') {
1880
+ if (aiKeyGroup) aiKeyGroup.style.display = 'flex';
1881
+ if (aiEndpointGroup) {
1882
+ aiEndpointGroup.style.display = 'flex';
1883
+ if (healerAiEndpoint) healerAiEndpoint.value = 'http://127.0.0.1:1234/v1';
1884
+ }
1885
+ } else {
1886
+ if (aiKeyGroup) aiKeyGroup.style.display = 'flex';
1887
+ if (aiEndpointGroup) aiEndpointGroup.style.display = 'none';
1888
+ }
1889
+ });
1890
+ }
1891
+
1892
+ if (healerEngineMode && healerEngineBadge) {
1893
+ healerEngineMode.addEventListener('change', () => {
1894
+ const mode = healerEngineMode.value;
1895
+ if (mode === 'ai') {
1896
+ healerEngineBadge.textContent = 'Engine: AI Healer Agent (Universal)';
1897
+ healerEngineBadge.className = 'engine-badge ai-agent';
1898
+ } else if (mode === 'codemod') {
1899
+ healerEngineBadge.textContent = 'Engine: Deterministic CodeMod';
1900
+ healerEngineBadge.className = 'engine-badge codemod';
1901
+ } else {
1902
+ healerEngineBadge.textContent = 'Engine: CodeMod + AI Hybrid';
1903
+ healerEngineBadge.className = 'engine-badge codemod';
1904
+ }
1905
+ });
1906
+ }
1907
+
1908
+ function getAiConfigPayload() {
1909
+ const engineMode = healerEngineMode ? healerEngineMode.value : 'hybrid';
1910
+ const provider = healerAiProvider ? healerAiProvider.value : 'ollama';
1911
+ const apiKey = healerAiKey ? healerAiKey.value.trim() : undefined;
1912
+ const endpoint = healerAiEndpoint ? healerAiEndpoint.value.trim() : undefined;
1913
+ return { engineMode, aiConfig: { provider, apiKey, endpoint } };
1914
+ }
1915
+
1916
+ if (btnHealerScan) {
1917
+ btnHealerScan.addEventListener('click', async () => {
1918
+ let rawDir = healerProjectDir ? healerProjectDir.value.trim() : '';
1919
+ if (!rawDir || rawDir === '.' || rawDir === './') {
1920
+ rawDir = 'examples/vulnerable-backend';
1921
+ if (healerProjectDir) healerProjectDir.value = rawDir;
1922
+ }
1923
+ const projectDir = rawDir;
1924
+ const { engineMode, aiConfig } = getAiConfigPayload();
1925
+
1926
+ btnHealerScan.disabled = true;
1927
+ btnHealerScan.textContent = 'Scanning...';
1928
+ if (healerScanStatus) {
1929
+ healerScanStatus.style.display = 'block';
1930
+ healerScanStatus.textContent = `Scanning project at "${projectDir}" using ${engineMode.toUpperCase()} engine...`;
1931
+ }
1932
+ if (healerDiffContainer) healerDiffContainer.style.display = 'none';
1933
+ if (healerActionsBar) healerActionsBar.style.display = 'none';
1934
+
1935
+ let failedChecks = [];
1936
+ if (lastRedTeamReport && lastRedTeamReport.breachesFound && lastRedTeamReport.breachesFound.length > 0) {
1937
+ failedChecks = Array.from(new Set(
1938
+ lastRedTeamReport.breachesFound.map(b => b.autoHealCheckKey || b.category || b.title).filter(Boolean)
1939
+ ));
1940
+ } else if (lastScorecardData && lastScorecardData.report) {
1941
+ const checks = lastScorecardData.report.checks || lastScorecardData.report.results || [];
1942
+ failedChecks = checks.filter(c => !c.passed).map(c => c.name);
1943
+ }
1944
+
1945
+ try {
1946
+ const res = await fetch('/_faultmesh/healer/scan', {
1947
+ method: 'POST',
1948
+ headers: { 'Content-Type': 'application/json' },
1949
+ body: JSON.stringify({ projectDir, failedChecks, engineMode, aiConfig }),
1950
+ });
1951
+ const result = await res.json();
1952
+ lastHealerScanResult = result;
1953
+
1954
+ if (!result.success) {
1955
+ if (healerScanStatus) {
1956
+ healerScanStatus.textContent = `Scan Notice: ${result.warnings.join('; ') || 'Could not locate server entry file'}`;
1957
+ healerScanStatus.style.borderColor = 'var(--tag-red-fg)';
1958
+ }
1959
+ return;
1960
+ }
1961
+
1962
+ if (result.patches.length === 0) {
1963
+ if (healerScanStatus) {
1964
+ healerScanStatus.innerHTML = `
1965
+ <div style="display: flex; flex-direction: column; gap: 6px;">
1966
+ <div><strong>Framework:</strong> ${escapeHtml(result.framework.toUpperCase())} (Entry: <code>${escapeHtml(result.entryFile)}</code>). All target defenses are already in place! Zero patches required.</div>
1967
+ <div style="font-size: 11px; color: var(--text-muted);">The backend codebase already has defensive headers, payload size limits, CORS restrictions, error sanitizers, socket timeouts, host whitelist guards, path traversal guards, and idempotency protection applied.</div>
1968
+ <div>
1969
+ <button id="btnQuickResetVulnerable" class="btn-secondary btn-sm" type="button" style="margin-top: 4px; font-weight: 600;">Reset Sample Backend to Vulnerable State (To Test Healing Again)</button>
1970
+ </div>
1971
+ </div>
1972
+ `;
1973
+ healerScanStatus.style.borderColor = 'var(--tag-green-fg)';
1974
+ const quickReset = document.getElementById('btnQuickResetVulnerable');
1975
+ if (quickReset && btnHealerResetSample) {
1976
+ quickReset.addEventListener('click', () => btnHealerResetSample.click());
1977
+ }
1978
+ }
1979
+ return;
1980
+ }
1981
+
1982
+ if (healerScanStatus) {
1983
+ const engineLabel = result.engineUsed === 'ai-agent' ? 'AI Agent' : (result.engineUsed === 'hybrid' ? 'Hybrid (CodeMod + AI)' : 'Deterministic CodeMod');
1984
+ healerScanStatus.textContent = `Detected ${result.framework.toUpperCase()} in "${result.entryFile}". Found ${result.patches.length} applicable remediations (Engine: ${engineLabel}):`;
1985
+ healerScanStatus.style.borderColor = 'var(--border-default)';
1986
+ }
1987
+
1988
+ if (healerDiffContainer) {
1989
+ healerDiffContainer.innerHTML = result.patches.map((p) => {
1990
+ const diffLines = p.diff.split(/\r?\n/).map(line => {
1991
+ if (line.startsWith('+') && !line.startsWith('+++')) {
1992
+ return `<span class="diff-line add">${escapeHtml(line)}</span>`;
1993
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
1994
+ return `<span class="diff-line del">${escapeHtml(line)}</span>`;
1995
+ } else if (line.startsWith('@@')) {
1996
+ return `<span class="diff-line info">${escapeHtml(line)}</span>`;
1997
+ } else if (line.startsWith('---') || line.startsWith('+++')) {
1998
+ return `<span class="diff-line file-header">${escapeHtml(line)}</span>`;
1999
+ }
2000
+ return `<span class="diff-line context">${escapeHtml(line)}</span>`;
2001
+ }).join('');
2002
+
2003
+ const engineTagClass = p.engine === 'ai-agent' ? 'ai-agent' : 'codemod';
2004
+ const engineTagLabel = p.engine === 'ai-agent' ? 'AI Agent' : 'CodeMod';
2005
+
2006
+ return `
2007
+ <div class="diff-card">
2008
+ <div class="diff-card-header">
2009
+ <div style="display: flex; align-items: center; gap: 8px;">
2010
+ <span class="diff-card-title">${escapeHtml(p.checkName)}</span>
2011
+ <span class="engine-badge ${engineTagClass}">${engineTagLabel}</span>
2012
+ </div>
2013
+ <span class="diff-card-file">${escapeHtml(p.relativePath)}</span>
2014
+ </div>
2015
+ <pre class="diff-pre"><code>${diffLines}</code></pre>
2016
+ </div>
2017
+ `;
2018
+ }).join('');
2019
+
2020
+ healerDiffContainer.style.display = 'flex';
2021
+ }
2022
+
2023
+ if (healerActionsBar) {
2024
+ healerActionsBar.style.display = 'flex';
2025
+ }
2026
+ } catch (err) {
2027
+ if (healerScanStatus) {
2028
+ healerScanStatus.textContent = `Scan Error: ${err.message}`;
2029
+ healerScanStatus.style.borderColor = 'var(--tag-red-fg)';
2030
+ }
2031
+ } finally {
2032
+ btnHealerScan.disabled = false;
2033
+ btnHealerScan.textContent = 'Scan Codebase';
2034
+ }
2035
+ });
2036
+ }
2037
+
2038
+ if (btnHealerApply) {
2039
+ btnHealerApply.addEventListener('click', async () => {
2040
+ if (!lastHealerScanResult || !lastHealerScanResult.patches) return;
2041
+ let rawDir = healerProjectDir ? healerProjectDir.value.trim() : '';
2042
+ if (!rawDir || rawDir === '.' || rawDir === './') {
2043
+ rawDir = 'examples/vulnerable-backend';
2044
+ if (healerProjectDir) healerProjectDir.value = rawDir;
2045
+ }
2046
+ const projectDir = rawDir;
2047
+ const createBackup = healerBackupCheck ? healerBackupCheck.checked : true;
2048
+ const { engineMode, aiConfig } = getAiConfigPayload();
2049
+
2050
+ let failedChecks = [];
2051
+ if (lastRedTeamReport && lastRedTeamReport.breachesFound && lastRedTeamReport.breachesFound.length > 0) {
2052
+ failedChecks = Array.from(new Set(
2053
+ lastRedTeamReport.breachesFound.map(b => b.autoHealCheckKey || b.category || b.title).filter(Boolean)
2054
+ ));
2055
+ } else if (lastScorecardData && lastScorecardData.report) {
2056
+ const checks = lastScorecardData.report.checks || lastScorecardData.report.results || [];
2057
+ failedChecks = checks.filter(c => !c.passed).map(c => c.name);
2058
+ }
2059
+
2060
+ const patchIds = lastHealerScanResult.patches.map(p => p.id);
2061
+
2062
+ btnHealerApply.disabled = true;
2063
+ btnHealerApply.textContent = 'Applying Remedies...';
2064
+
2065
+ try {
2066
+ const res = await fetch('/_faultmesh/healer/apply', {
2067
+ method: 'POST',
2068
+ headers: { 'Content-Type': 'application/json' },
2069
+ body: JSON.stringify({ projectDir, patchIds, failedChecks, createBackup, engineMode, aiConfig }),
2070
+ });
2071
+ const result = await res.json();
2072
+
2073
+ if (result.success) {
2074
+ if (result.appliedCount === 0) {
2075
+ if (healerScanStatus) {
2076
+ healerScanStatus.innerHTML = `<div>Notice: Zero patches were applied to disk. ${(result.errors || []).join('; ')}</div>`;
2077
+ healerScanStatus.style.borderColor = 'var(--tag-amber-fg)';
2078
+ }
2079
+ return;
2080
+ }
2081
+
2082
+ lastBackupDir = result.backupDir;
2083
+ if (projectDir.includes('vulnerable-backend')) {
2084
+ applyTargetUrl('http://127.0.0.1:5050');
2085
+ }
2086
+
2087
+ if (healerScanStatus) {
2088
+ healerScanStatus.textContent = `Successfully applied ${result.appliedCount} remedies! Target switched to healed backend on http://127.0.0.1:5050. Re-running validation...`;
2089
+ healerScanStatus.style.borderColor = 'var(--tag-green-fg)';
2090
+ }
2091
+ if (healerDiffContainer) healerDiffContainer.style.display = 'none';
2092
+ btnHealerApply.style.display = 'none';
2093
+
2094
+ if (btnHealerRollback && lastBackupDir) {
2095
+ btnHealerRollback.style.display = 'inline-block';
2096
+ }
2097
+
2098
+ // Trigger live re-test automatically
2099
+ setTimeout(() => {
2100
+ if (currentDiagMode === 'redteam') {
2101
+ if (healerModal) healerModal.style.display = 'none';
2102
+ startRedTeamCampaign();
2103
+ } else if (btnRunDiagnostics) {
2104
+ btnRunDiagnostics.click();
2105
+ }
2106
+ }, 800);
2107
+ } else {
2108
+ if (healerScanStatus) {
2109
+ healerScanStatus.textContent = `Apply Error: ${(result.errors || []).join('; ')}`;
2110
+ healerScanStatus.style.borderColor = 'var(--tag-red-fg)';
2111
+ }
2112
+ }
2113
+ } catch (err) {
2114
+ if (healerScanStatus) {
2115
+ healerScanStatus.textContent = `Network Error: ${err.message}`;
2116
+ healerScanStatus.style.borderColor = 'var(--tag-red-fg)';
2117
+ }
2118
+ } finally {
2119
+ btnHealerApply.disabled = false;
2120
+ btnHealerApply.textContent = 'Apply Remedies to Codebase';
2121
+ }
2122
+ });
2123
+ }
2124
+
2125
+ if (btnHealerRollback) {
2126
+ btnHealerRollback.addEventListener('click', async () => {
2127
+ if (!lastBackupDir) return;
2128
+ const projectDir = (healerProjectDir ? healerProjectDir.value.trim() : '') || '.';
2129
+ btnHealerRollback.disabled = true;
2130
+ btnHealerRollback.textContent = 'Rolling back...';
2131
+
2132
+ try {
2133
+ const res = await fetch('/_faultmesh/healer/rollback', {
2134
+ method: 'POST',
2135
+ headers: { 'Content-Type': 'application/json' },
2136
+ body: JSON.stringify({ projectDir, backupDir: lastBackupDir }),
2137
+ });
2138
+ const result = await res.json();
2139
+ if (result.success) {
2140
+ if (healerScanStatus) {
2141
+ healerScanStatus.textContent = `Rollback complete. Restored original files. Backend restarted on port 5050. Re-running live tests...`;
2142
+ healerScanStatus.style.borderColor = 'var(--border-default)';
2143
+ }
2144
+ btnHealerRollback.style.display = 'none';
2145
+ if (btnHealerApply) btnHealerApply.style.display = 'inline-block';
2146
+
2147
+ // Trigger live re-test automatically to reflect rolled-back state
2148
+ setTimeout(() => {
2149
+ if (btnRunDiagnostics) {
2150
+ btnRunDiagnostics.click();
2151
+ }
2152
+ }, 600);
2153
+ } else {
2154
+ if (healerScanStatus) {
2155
+ healerScanStatus.textContent = `Rollback error: ${(result.errors || []).join('; ')}`;
2156
+ }
2157
+ }
2158
+ } catch (err) {
2159
+ if (healerScanStatus) {
2160
+ healerScanStatus.textContent = `Rollback failed: ${err.message}`;
2161
+ }
2162
+ } finally {
2163
+ btnHealerRollback.disabled = false;
2164
+ btnHealerRollback.textContent = 'Rollback Last Patch';
2165
+ }
2166
+ });
2167
+ }
2168
+
2169
+ const btnHealerResetSample = document.getElementById('btnHealerResetSample');
2170
+ if (btnHealerResetSample) {
2171
+ btnHealerResetSample.addEventListener('click', async () => {
2172
+ btnHealerResetSample.disabled = true;
2173
+ btnHealerResetSample.textContent = 'Resetting...';
2174
+ try {
2175
+ const res = await fetch('/_faultmesh/sample/reset', { method: 'POST' });
2176
+ const data = await res.json();
2177
+ applyTargetUrl('http://127.0.0.1:5050');
2178
+ if (healerProjectDir) healerProjectDir.value = 'examples/vulnerable-backend';
2179
+ if (healerScanStatus) {
2180
+ healerScanStatus.style.display = 'block';
2181
+ healerScanStatus.textContent = 'Sample backend reset to vulnerable baseline! Re-scanning...';
2182
+ healerScanStatus.style.borderColor = 'var(--tag-amber-fg)';
2183
+ }
2184
+ setTimeout(() => {
2185
+ if (btnHealerScan) btnHealerScan.click();
2186
+ }, 500);
2187
+ } catch (err) {
2188
+ alert('Failed to reset sample backend: ' + err.message);
2189
+ } finally {
2190
+ btnHealerResetSample.disabled = false;
2191
+ btnHealerResetSample.textContent = 'Reset Sample to Vulnerable';
2192
+ }
2193
+ });
2194
+ }
2195
+
2196
+ // 9. Live Request SSE Stream
2197
+ function setupSSE() {
2198
+ const eventSource = new EventSource('/_faultmesh/telemetry/stream');
2199
+
2200
+ eventSource.addEventListener('snapshot', (e) => {
2201
+ try {
2202
+ const data = JSON.parse(e.data);
2203
+ if (data.events && data.events.length > 0) {
2204
+ activityFeed.innerHTML = '';
2205
+ data.events.forEach(addLogRow);
2206
+ }
2207
+ } catch (err) {
2208
+ console.warn('Snapshot parse error', err);
2209
+ }
2210
+ });
2211
+
2212
+ eventSource.addEventListener('telemetry', (e) => {
2213
+ try {
2214
+ const event = JSON.parse(e.data);
2215
+ addLogRow(event);
2216
+ refreshStatus();
2217
+ } catch (err) {
2218
+ console.warn('Telemetry parse error', err);
2219
+ }
2220
+ });
2221
+
2222
+ eventSource.addEventListener('redteam-progress', (e) => {
2223
+ try {
2224
+ const status = JSON.parse(e.data);
2225
+ updateRedTeamHud(status);
2226
+ } catch (err) {
2227
+ console.warn('Red team progress parse error', err);
2228
+ }
2229
+ });
2230
+
2231
+ eventSource.addEventListener('redteam-wave-start', (e) => {
2232
+ try {
2233
+ const payload = JSON.parse(e.data);
2234
+ if (payload.wave) {
2235
+ highlightActivePersona(payload.wave.personaId);
2236
+ }
2237
+ if (payload.status) {
2238
+ updateRedTeamHud(payload.status);
2239
+ }
2240
+ } catch (err) {
2241
+ console.warn('Red team wave start parse error', err);
2242
+ }
2243
+ });
2244
+
2245
+ eventSource.addEventListener('redteam-log', (e) => {
2246
+ try {
2247
+ const log = JSON.parse(e.data);
2248
+ appendRedTeamLog(log);
2249
+ } catch (err) {
2250
+ console.warn('Red team log parse error', err);
2251
+ }
2252
+ });
2253
+
2254
+ eventSource.addEventListener('redteam-breach', (e) => {
2255
+ try {
2256
+ const payload = JSON.parse(e.data);
2257
+ if (payload.status) {
2258
+ updateRedTeamHud(payload.status);
2259
+ }
2260
+ } catch (err) {
2261
+ console.warn('Red team breach parse error', err);
2262
+ }
2263
+ });
2264
+
2265
+ eventSource.addEventListener('redteam-complete', (e) => {
2266
+ try {
2267
+ const payload = JSON.parse(e.data);
2268
+ if (payload.report) {
2269
+ renderRedTeamDossier(payload.report);
2270
+ }
2271
+ if (payload.status) {
2272
+ updateRedTeamHud(payload.status);
2273
+ }
2274
+ stopRedTeamPolling();
2275
+ } catch (err) {
2276
+ console.warn('Red team complete parse error', err);
2277
+ }
2278
+ });
2279
+ }
2280
+
2281
+ function addLogRow(ev) {
2282
+ if (activityFeed.querySelector('.table-empty')) {
2283
+ activityFeed.innerHTML = '';
2284
+ }
2285
+
2286
+ const tr = document.createElement('tr');
2287
+ let codeClass = 'code-2xx';
2288
+ if (ev.statusCode >= 500) codeClass = 'code-5xx';
2289
+ else if (ev.statusCode >= 400) codeClass = 'code-4xx';
2290
+
2291
+ const ruleText = ev.appliedToxics && ev.appliedToxics.length > 0
2292
+ ? ev.appliedToxics.join(', ')
969
2293
  : 'None';
970
2294
 
971
2295
  tr.innerHTML = `
@@ -986,13 +2310,456 @@ document.addEventListener('DOMContentLoaded', () => {
986
2310
  activityFeed.innerHTML = '<tr><td colspan="5" class="table-empty">Log cleared. Listening for requests...</td></tr>';
987
2311
  });
988
2312
 
2313
+ // =========================================================================
2314
+ // AUTONOMOUS RED CHAOS TEAM ENGINE CONTROLLER
2315
+ // =========================================================================
2316
+
2317
+ async function loadRedTeamPersonas() {
2318
+ if (!redTeamPersonasGrid) return;
2319
+ try {
2320
+ const res = await fetch('/_faultmesh/redteam/personas');
2321
+ if (!res.ok) return;
2322
+ const data = await res.json();
2323
+ if (data.personas && data.personas.length > 0) {
2324
+ redTeamPersonasGrid.innerHTML = data.personas.map(p => `
2325
+ <div class="persona-card" data-persona="${escapeHtml(p.id)}">
2326
+ <div class="persona-top">
2327
+ <span class="persona-callsign">${escapeHtml(p.callSign || 'AGENT')}</span>
2328
+ <span class="persona-role-tag">${escapeHtml(p.specialty || 'Chaos')}</span>
2329
+ </div>
2330
+ <strong class="persona-name">${escapeHtml(p.name)}</strong>
2331
+ <p class="persona-desc">${escapeHtml(p.description)}</p>
2332
+ </div>
2333
+ `).join('');
2334
+ }
2335
+ } catch (err) {
2336
+ console.warn('Failed to load personas from bridge:', err);
2337
+ }
2338
+ }
2339
+
2340
+ async function refreshRedTeamStatus() {
2341
+ try {
2342
+ const res = await fetch('/_faultmesh/redteam/status');
2343
+ if (!res.ok) return;
2344
+ const status = await res.json();
2345
+ if (status.active) {
2346
+ if (redTeamHud) redTeamHud.style.display = 'flex';
2347
+ if (redTeamDossier) redTeamDossier.style.display = 'none';
2348
+ if (btnLaunchRedTeam) btnLaunchRedTeam.style.display = 'none';
2349
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'inline-flex';
2350
+ updateRedTeamHud(status);
2351
+ startRedTeamPolling();
2352
+ } else if (status.phase === 'complete') {
2353
+ const repRes = await fetch('/_faultmesh/redteam/report');
2354
+ if (repRes.ok) {
2355
+ const report = await repRes.json();
2356
+ if (report.campaignId) {
2357
+ renderRedTeamDossier(report);
2358
+ }
2359
+ }
2360
+ }
2361
+ } catch (err) {
2362
+ console.warn('Failed to check red team status:', err);
2363
+ }
2364
+ }
2365
+
2366
+ async function startRedTeamCampaign() {
2367
+ if (!btnLaunchRedTeam) return;
2368
+ const intensity = redTeamIntensitySelect ? redTeamIntensitySelect.value : 'sustained';
2369
+ // Probes must route through the Chaos Proxy so wave toxics are actively engaged
2370
+ const targetUrl = 'http://127.0.0.1:3001';
2371
+
2372
+ btnLaunchRedTeam.style.display = 'none';
2373
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'inline-flex';
2374
+ if (redTeamHud) redTeamHud.style.display = 'flex';
2375
+ if (redTeamDossier) redTeamDossier.style.display = 'none';
2376
+
2377
+ seenLogKeys.clear();
2378
+ if (redTeamConsoleLogs) {
2379
+ redTeamConsoleLogs.innerHTML = '';
2380
+ appendRedTeamLog({
2381
+ timestamp: Date.now(),
2382
+ waveNumber: 0,
2383
+ persona: 'RED-COMMAND',
2384
+ message: `Deploying Autonomous Red Chaos Team [${intensity.toUpperCase()} assault mode] against ${targetUrl}...`,
2385
+ type: 'info',
2386
+ });
2387
+ }
2388
+
2389
+ try {
2390
+ const res = await fetch('/_faultmesh/redteam/start', {
2391
+ method: 'POST',
2392
+ headers: { 'Content-Type': 'application/json' },
2393
+ body: JSON.stringify({ intensity, targetUrl }),
2394
+ });
2395
+ const data = await res.json();
2396
+ if (!res.ok || !data.success) {
2397
+ alert('Failed to launch Red Team: ' + (data.error || 'Server error'));
2398
+ btnLaunchRedTeam.style.display = 'inline-flex';
2399
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'none';
2400
+ return;
2401
+ }
2402
+ if (data.status) {
2403
+ updateRedTeamHud(data.status);
2404
+ }
2405
+ startRedTeamPolling();
2406
+ } catch (err) {
2407
+ alert('Network error launching Red Team: ' + err.message);
2408
+ btnLaunchRedTeam.style.display = 'inline-flex';
2409
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'none';
2410
+ }
2411
+ }
2412
+
2413
+ async function abortRedTeamCampaign() {
2414
+ if (btnAbortRedTeam) {
2415
+ btnAbortRedTeam.disabled = true;
2416
+ btnAbortRedTeam.textContent = 'Aborting...';
2417
+ }
2418
+ try {
2419
+ await fetch('/_faultmesh/redteam/abort', { method: 'POST' });
2420
+ stopRedTeamPolling();
2421
+ appendRedTeamLog({
2422
+ timestamp: Date.now(),
2423
+ waveNumber: 0,
2424
+ persona: 'COMMAND-ABORT',
2425
+ message: 'Abort requested. Neutralized assault waves and flushed toxic proxy pipeline.',
2426
+ type: 'warn',
2427
+ });
2428
+ } catch (err) {
2429
+ console.warn('Abort error:', err);
2430
+ } finally {
2431
+ if (btnAbortRedTeam) {
2432
+ btnAbortRedTeam.style.display = 'none';
2433
+ btnAbortRedTeam.disabled = false;
2434
+ btnAbortRedTeam.textContent = 'Abort Mission';
2435
+ }
2436
+ if (btnLaunchRedTeam) btnLaunchRedTeam.style.display = 'inline-flex';
2437
+ }
2438
+ }
2439
+
2440
+ function startRedTeamPolling() {
2441
+ if (redTeamPollingInterval) return;
2442
+ redTeamPollingInterval = setInterval(async () => {
2443
+ try {
2444
+ const res = await fetch('/_faultmesh/redteam/status');
2445
+ if (!res.ok) return;
2446
+ const status = await res.json();
2447
+ updateRedTeamHud(status);
2448
+ if (!status.active) {
2449
+ stopRedTeamPolling();
2450
+ if (btnLaunchRedTeam) btnLaunchRedTeam.style.display = 'inline-flex';
2451
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'none';
2452
+ if (status.phase === 'complete') {
2453
+ const repRes = await fetch('/_faultmesh/redteam/report');
2454
+ if (repRes.ok) {
2455
+ const rep = await repRes.json();
2456
+ if (rep.campaignId) renderRedTeamDossier(rep);
2457
+ }
2458
+ }
2459
+ }
2460
+ } catch (err) {
2461
+ console.warn('Polling error:', err);
2462
+ }
2463
+ }, 1000);
2464
+ }
2465
+
2466
+ function stopRedTeamPolling() {
2467
+ if (redTeamPollingInterval) {
2468
+ clearInterval(redTeamPollingInterval);
2469
+ redTeamPollingInterval = null;
2470
+ }
2471
+ }
2472
+
2473
+ function updateRedTeamHud(status) {
2474
+ if (!status) return;
2475
+
2476
+ if (redTeamProgressBar) {
2477
+ redTeamProgressBar.style.width = `${status.progressPercent}%`;
2478
+ }
2479
+ if (redTeamProgressPercent) {
2480
+ redTeamProgressPercent.textContent = `${status.progressPercent}%`;
2481
+ }
2482
+ if (redTeamCurrentWaveText) {
2483
+ redTeamCurrentWaveText.textContent = status.currentWaveName || `Phase: ${status.phase}`;
2484
+ }
2485
+ if (redTeamActivePersonaPill) {
2486
+ redTeamActivePersonaPill.textContent = status.activePersona ? `Active: ${status.activePersona}` : 'Standby';
2487
+ }
2488
+
2489
+ if (redTeamProbesCount) redTeamProbesCount.textContent = status.probesSent;
2490
+ if (status.breachesCount) {
2491
+ if (redTeamCritCount) redTeamCritCount.textContent = status.breachesCount.critical;
2492
+ if (redTeamHighCount) redTeamHighCount.textContent = status.breachesCount.high;
2493
+ if (redTeamMedCount) redTeamMedCount.textContent = status.breachesCount.medium;
2494
+ }
2495
+
2496
+ // Update phase step breadcrumbs
2497
+ const steps = document.querySelectorAll('.phase-step');
2498
+ const phaseOrder = ['recon', 'weaponize', 'assault', 'debrief', 'complete'];
2499
+ const currentIdx = phaseOrder.indexOf(status.phase);
2500
+
2501
+ steps.forEach((step) => {
2502
+ const p = step.getAttribute('data-phase');
2503
+ const stepIdx = phaseOrder.indexOf(p);
2504
+ step.classList.remove('active', 'completed');
2505
+ if (stepIdx < currentIdx || status.phase === 'complete') {
2506
+ step.classList.add('completed');
2507
+ } else if (p === status.phase) {
2508
+ step.classList.add('active');
2509
+ }
2510
+ });
2511
+
2512
+ if (status.latestLog && redTeamConsoleLogs) {
2513
+ appendRedTeamLog(status.latestLog);
2514
+ }
2515
+ }
2516
+
2517
+ function highlightActivePersona(personaId) {
2518
+ const cards = document.querySelectorAll('.persona-card');
2519
+ cards.forEach(card => {
2520
+ if (card.getAttribute('data-persona') === personaId) {
2521
+ card.classList.add('persona-active');
2522
+ } else {
2523
+ card.classList.remove('persona-active');
2524
+ }
2525
+ });
2526
+ }
2527
+
2528
+ const seenLogKeys = new Set();
2529
+
2530
+ function appendRedTeamLog(log) {
2531
+ if (!redTeamConsoleLogs || !log || !log.message) return;
2532
+ const logKey = `${log.timestamp || ''}_${log.waveNumber ?? ''}_${log.persona || ''}_${log.message}`;
2533
+ if (seenLogKeys.has(logKey)) {
2534
+ return;
2535
+ }
2536
+ seenLogKeys.add(logKey);
2537
+ if (seenLogKeys.size > 200) {
2538
+ const first = seenLogKeys.values().next().value;
2539
+ if (first) seenLogKeys.delete(first);
2540
+ }
2541
+
2542
+ const time = new Date(log.timestamp || Date.now()).toTimeString().split(' ')[0];
2543
+ const line = document.createElement('div');
2544
+ line.className = `terminal-line log-${log.type || 'info'}`;
2545
+ line.textContent = `[${time}] [${log.persona || 'RED-TEAM'}] ${log.message}`;
2546
+
2547
+ redTeamConsoleLogs.appendChild(line);
2548
+ while (redTeamConsoleLogs.children.length > 80) {
2549
+ redTeamConsoleLogs.removeChild(redTeamConsoleLogs.firstChild);
2550
+ }
2551
+ redTeamConsoleLogs.scrollTop = redTeamConsoleLogs.scrollHeight;
2552
+ }
2553
+
2554
+ function renderRedTeamDossier(report) {
2555
+ lastRedTeamReport = report;
2556
+ if (!redTeamDossier) return;
2557
+
2558
+ redTeamDossier.style.display = 'flex';
2559
+ if (btnLaunchRedTeam) btnLaunchRedTeam.style.display = 'inline-flex';
2560
+ if (btnAbortRedTeam) btnAbortRedTeam.style.display = 'none';
2561
+
2562
+ if (dossierScoreVal) dossierScoreVal.textContent = `${report.survivabilityScore}/100`;
2563
+ if (dossierGradeVal) {
2564
+ dossierGradeVal.textContent = `GRADE ${report.survivabilityGrade}`;
2565
+ dossierGradeVal.className = `dossier-grade-tag grade-${report.survivabilityGrade}`;
2566
+ }
2567
+
2568
+ if (dossierTitle) {
2569
+ dossierTitle.textContent = `Mission Debrief: ${report.breachesFound.length} Breaches Identified`;
2570
+ }
2571
+ if (dossierSummaryText) {
2572
+ dossierSummaryText.textContent = `System survivability score: ${report.survivabilityScore}/100 [Grade ${report.survivabilityGrade}]. Dispatched ${report.totalProbes} adversarial probes across ${report.totalWaves} compound assault waves in ${Math.round(report.durationMs / 1000)}s.`;
2573
+ }
2574
+ if (dossierFindingsCount) {
2575
+ dossierFindingsCount.textContent = `${report.breachesFound.length} breaches found`;
2576
+ }
2577
+
2578
+ if (dossierFindingsList) {
2579
+ if (report.breachesFound.length === 0) {
2580
+ dossierFindingsList.innerHTML = '<div class="empty-state">Zero breaches detected. Target system sustained all adversarial compound assault waves!</div>';
2581
+ } else {
2582
+ dossierFindingsList.innerHTML = report.breachesFound.map(b => `
2583
+ <div class="breach-card severity-${b.severity}">
2584
+ <div class="breach-card-top">
2585
+ <div class="breach-title-wrap">
2586
+ <span class="severity-pill severity-${b.severity}">${b.severity.toUpperCase()}</span>
2587
+ <span class="breach-title">${escapeHtml(b.title)}</span>
2588
+ <span class="category-pill">${escapeHtml(b.category)}</span>
2589
+ </div>
2590
+ <span class="persona-callsign">${escapeHtml(b.callSign || 'EXPLOIT')}</span>
2591
+ </div>
2592
+ <div class="breach-desc">${escapeHtml(b.impact)}</div>
2593
+ <div class="breach-proof-box">
2594
+ <div><strong>Target Endpoint:</strong> ${escapeHtml(b.endpoint)}</div>
2595
+ <div><strong>Probe Response:</strong> Status ${b.proofOfBreach.responseStatus} in ${b.proofOfBreach.responseDurationMs}ms</div>
2596
+ <div><strong>Evidence Snippet:</strong> ${escapeHtml(b.proofOfBreach.snippet || '(empty)')}</div>
2597
+ </div>
2598
+ <div>
2599
+ <span class="breach-remedy"><strong>Remedy:</strong> ${escapeHtml(b.remediation)}</span>
2600
+ </div>
2601
+ </div>
2602
+ `).join('');
2603
+ }
2604
+ }
2605
+
2606
+ redTeamDossier.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2607
+ }
2608
+
2609
+ function autoHealDiscoveredBreaches() {
2610
+ if (!lastRedTeamReport || !lastRedTeamReport.breachesFound || lastRedTeamReport.breachesFound.length === 0) {
2611
+ alert('No active breaches in report to heal.');
2612
+ return;
2613
+ }
2614
+
2615
+ const failedCheckNames = Array.from(new Set(
2616
+ lastRedTeamReport.breachesFound
2617
+ .map(b => b.autoHealCheckKey)
2618
+ .filter(Boolean)
2619
+ ));
2620
+
2621
+ // Open healer modal and configure
2622
+ if (healerProjectDir && (!healerProjectDir.value || healerProjectDir.value === '.')) {
2623
+ healerProjectDir.value = 'examples/vulnerable-backend';
2624
+ }
2625
+ if (healerModal) {
2626
+ healerModal.style.display = 'flex';
2627
+ }
2628
+ if (healerScanStatus) {
2629
+ healerScanStatus.style.display = 'block';
2630
+ healerScanStatus.innerHTML = `Loaded <strong>${failedCheckNames.length}</strong> targeted breaches from Red Team Dossier: <code>${failedCheckNames.join(', ')}</code>. Initializing CodeMod scanner...`;
2631
+ }
2632
+
2633
+ setTimeout(() => {
2634
+ if (btnHealerScan) {
2635
+ btnHealerScan.click();
2636
+ }
2637
+ }, 400);
2638
+ }
2639
+
2640
+ function exportDossier(format) {
2641
+ if (!lastRedTeamReport) {
2642
+ alert('No completed campaign report to export.');
2643
+ return;
2644
+ }
2645
+
2646
+ let content = '';
2647
+ let filename = `faultmesh-redteam-dossier-${lastRedTeamReport.campaignId}.${format === 'json' ? 'json' : 'md'}`;
2648
+ let type = format === 'json' ? 'application/json' : 'text/markdown';
2649
+
2650
+ if (format === 'json') {
2651
+ content = JSON.stringify(lastRedTeamReport, null, 2);
2652
+ } else {
2653
+ content = [
2654
+ `# FaultMesh Red Team Mission Debrief Dossier`,
2655
+ ``,
2656
+ `**Campaign ID:** ${lastRedTeamReport.campaignId}`,
2657
+ `**Target URL:** ${lastRedTeamReport.targetUrl}`,
2658
+ `**Intensity:** ${lastRedTeamReport.intensity.toUpperCase()}`,
2659
+ `**Survivability Score:** ${lastRedTeamReport.survivabilityScore}/100 (Grade ${lastRedTeamReport.survivabilityGrade})`,
2660
+ `**Probes Dispatched:** ${lastRedTeamReport.totalProbes}`,
2661
+ `**Breaches Discovered:** ${lastRedTeamReport.breachesFound.length}`,
2662
+ `**Duration:** ${Math.round(lastRedTeamReport.durationMs / 1000)} seconds`,
2663
+ ``,
2664
+ `## Active Personas`,
2665
+ ...lastRedTeamReport.activePersonas.map(p => `- ${p}`),
2666
+ ``,
2667
+ `## Discovered Breaches`,
2668
+ ...lastRedTeamReport.breachesFound.map((b, i) => [
2669
+ `### ${i + 1}. [${b.severity.toUpperCase()}] ${b.title}`,
2670
+ `- **Category:** ${b.category}`,
2671
+ `- **Persona:** ${b.personaName} (${b.callSign})`,
2672
+ `- **Endpoint:** ${b.endpoint}`,
2673
+ `- **Impact:** ${b.impact}`,
2674
+ `- **Remediation:** ${b.remediation}`,
2675
+ `- **Evidence:** Status ${b.proofOfBreach.responseStatus} in ${b.proofOfBreach.responseDurationMs}ms - \`${b.proofOfBreach.snippet}\``,
2676
+ ``,
2677
+ ].join('\n')),
2678
+ ].join('\n');
2679
+ }
2680
+
2681
+ const blob = new Blob([content], { type });
2682
+ const url = URL.createObjectURL(blob);
2683
+ const a = document.createElement('a');
2684
+ a.href = url;
2685
+ a.download = filename;
2686
+ document.body.appendChild(a);
2687
+ a.click();
2688
+ document.body.removeChild(a);
2689
+ URL.revokeObjectURL(url);
2690
+ }
2691
+
2692
+ if (btnLaunchRedTeam) btnLaunchRedTeam.addEventListener('click', startRedTeamCampaign);
2693
+ if (btnAbortRedTeam) btnAbortRedTeam.addEventListener('click', abortRedTeamCampaign);
2694
+ if (btnAutoHealBreaches) btnAutoHealBreaches.addEventListener('click', autoHealDiscoveredBreaches);
2695
+ if (btnExportDossierMd) btnExportDossierMd.addEventListener('click', () => exportDossier('md'));
2696
+ if (btnExportDossierJson) btnExportDossierJson.addEventListener('click', () => exportDossier('json'));
2697
+
989
2698
  function escapeHtml(str) {
990
2699
  return String(str || '').replace(/[&<>"']/g, m => ({
991
2700
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
992
2701
  }[m]));
993
2702
  }
994
2703
 
2704
+ // Synchronize Workbench Heights: Right column matches Left column perfectly
2705
+ function syncWorkbenchHeights() {
2706
+ const presetsCard = document.querySelector('.presets-card');
2707
+ const customRuleCard = document.querySelector('.custom-rule-card');
2708
+ const activeRulesCard = document.querySelector('.active-rules-card');
2709
+ const responseInspector = document.getElementById('responseInspector');
2710
+ const liveLogCard = document.querySelector('.live-log-card');
2711
+ const leftWorkbenchCol = document.getElementById('leftWorkbenchCol');
2712
+
2713
+ if (!presetsCard || !customRuleCard || !activeRulesCard || !responseInspector || !liveLogCard) {
2714
+ return;
2715
+ }
2716
+
2717
+ if (window.innerWidth <= 1024) {
2718
+ responseInspector.style.height = 'auto';
2719
+ liveLogCard.style.height = 'auto';
2720
+ return;
2721
+ }
2722
+
2723
+ const presetsHeight = presetsCard.offsetHeight;
2724
+ const customHeight = customRuleCard.offsetHeight;
2725
+ const activeHeight = activeRulesCard.offsetHeight;
2726
+
2727
+ // Detect gap between cards in left column (default 16px)
2728
+ let gap = 16;
2729
+ if (leftWorkbenchCol) {
2730
+ const colStyle = window.getComputedStyle(leftWorkbenchCol);
2731
+ const parsedGap = parseFloat(colStyle.rowGap || colStyle.gap);
2732
+ if (!isNaN(parsedGap) && parsedGap > 0) {
2733
+ gap = parsedGap;
2734
+ }
2735
+ }
2736
+
2737
+ // 1. Response Inspector height matches the 1-Click Quick Presets card exactly
2738
+ responseInspector.style.height = `${presetsHeight}px`;
2739
+
2740
+ // 2. Live Traffic Log starts at Custom Rule card and ends at Active Simulation Rules card
2741
+ const targetLogHeight = customHeight + gap + activeHeight;
2742
+ liveLogCard.style.height = `${targetLogHeight}px`;
2743
+ }
2744
+
2745
+ if (typeof ResizeObserver !== 'undefined') {
2746
+ const workbenchResizeObserver = new ResizeObserver(() => {
2747
+ syncWorkbenchHeights();
2748
+ });
2749
+ const presetsEl = document.querySelector('.presets-card');
2750
+ const customEl = document.querySelector('.custom-rule-card');
2751
+ const activeEl = document.querySelector('.active-rules-card');
2752
+ if (presetsEl) workbenchResizeObserver.observe(presetsEl);
2753
+ if (customEl) workbenchResizeObserver.observe(customEl);
2754
+ if (activeEl) workbenchResizeObserver.observe(activeEl);
2755
+ }
2756
+
2757
+ window.addEventListener('resize', syncWorkbenchHeights);
2758
+
995
2759
  setDiagMode('resilience');
2760
+ loadTargetUrl();
996
2761
  refreshStatus();
997
2762
  setupSSE();
2763
+ // Initial height synchronization after DOM render
2764
+ setTimeout(syncWorkbenchHeights, 50);
998
2765
  });