faultmesh 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -19
- package/dist/dashboard/app.js +1906 -139
- package/dist/dashboard/index.html +393 -99
- package/dist/dashboard/styles.css +1191 -23
- package/dist/engine/ControlApi.d.ts +13 -1
- package/dist/engine/ControlApi.js +304 -14
- package/dist/engine/FaultMeshProxy.d.ts +2 -0
- package/dist/engine/FaultMeshProxy.js +41 -5
- package/dist/engine/TelemetryHub.d.ts +1 -0
- package/dist/engine/TelemetryHub.js +3 -0
- package/dist/engine/ToxicPipeline.d.ts +5 -4
- package/dist/engine/ToxicPipeline.js +17 -4
- package/dist/healer/AiHealer.d.ts +30 -0
- package/dist/healer/AiHealer.js +188 -0
- package/dist/healer/AutoHealer.d.ts +24 -0
- package/dist/healer/AutoHealer.js +503 -0
- package/dist/healer/DiffGenerator.d.ts +10 -0
- package/dist/healer/DiffGenerator.js +63 -0
- package/dist/healer/DiffUtil.d.ts +6 -0
- package/dist/healer/DiffUtil.js +67 -0
- package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
- package/dist/healer/transformers/ExpressTransformers.js +228 -0
- package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
- package/dist/healer/transformers/FastApiTransformers.js +93 -0
- package/dist/healer/transformers/GoTransformers.d.ts +19 -0
- package/dist/healer/transformers/GoTransformers.js +106 -0
- package/dist/healer/types.d.ts +59 -0
- package/dist/healer/types.js +1 -0
- package/dist/redteam/EccAgentBridge.d.ts +20 -0
- package/dist/redteam/EccAgentBridge.js +303 -0
- package/dist/redteam/RedTeamEngine.d.ts +56 -0
- package/dist/redteam/RedTeamEngine.js +709 -0
- package/dist/redteam/types.d.ts +117 -0
- package/dist/redteam/types.js +5 -0
- package/dist/scorer/ResilienceScorer.d.ts +5 -0
- package/dist/scorer/ResilienceScorer.js +323 -7
- package/dist/scorer/SecurityAuditor.d.ts +8 -0
- package/dist/scorer/SecurityAuditor.js +471 -69
- package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
- package/dist/scorer/TrafficStormAuditor.js +328 -69
- package/dist/server.js +17 -10
- package/dist/types.d.ts +7 -3
- package/package.json +2 -1
package/dist/dashboard/app.js
CHANGED
|
@@ -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 = '
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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':
|
|
242
|
-
case 'bandwidth':
|
|
243
|
-
case 'cut':
|
|
244
|
-
case 'corrupt':
|
|
245
|
-
case 'status':
|
|
246
|
-
default:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1015
|
+
if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 10-Point Resilience Benchmark';
|
|
650
1016
|
scoreTitle.textContent = 'Ready to evaluate resilience';
|
|
651
|
-
scoreSubtitle.textContent = 'Click "Run
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
958
|
-
if (
|
|
959
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
968
|
-
|
|
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
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
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
|
});
|