faultmesh 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +105 -0
  4. package/dist/dashboard/app.js +998 -0
  5. package/dist/dashboard/index.html +291 -0
  6. package/dist/dashboard/styles.css +1120 -0
  7. package/dist/engine/ControlApi.d.ts +24 -0
  8. package/dist/engine/ControlApi.js +269 -0
  9. package/dist/engine/FaultMeshProxy.d.ts +16 -0
  10. package/dist/engine/FaultMeshProxy.js +167 -0
  11. package/dist/engine/TelemetryHub.d.ts +16 -0
  12. package/dist/engine/TelemetryHub.js +67 -0
  13. package/dist/engine/ToxicPipeline.d.ts +22 -0
  14. package/dist/engine/ToxicPipeline.js +67 -0
  15. package/dist/scorer/ResilienceScorer.d.ts +13 -0
  16. package/dist/scorer/ResilienceScorer.js +330 -0
  17. package/dist/scorer/SecurityAuditor.d.ts +13 -0
  18. package/dist/scorer/SecurityAuditor.js +428 -0
  19. package/dist/scorer/TrafficStormAuditor.d.ts +10 -0
  20. package/dist/scorer/TrafficStormAuditor.js +261 -0
  21. package/dist/server.d.ts +21 -0
  22. package/dist/server.js +169 -0
  23. package/dist/toxics/BandwidthToxic.d.ts +11 -0
  24. package/dist/toxics/BandwidthToxic.js +36 -0
  25. package/dist/toxics/BaseToxic.d.ts +11 -0
  26. package/dist/toxics/BaseToxic.js +19 -0
  27. package/dist/toxics/CorruptToxic.d.ts +11 -0
  28. package/dist/toxics/CorruptToxic.js +53 -0
  29. package/dist/toxics/CutToxic.d.ts +9 -0
  30. package/dist/toxics/CutToxic.js +31 -0
  31. package/dist/toxics/LatencyToxic.d.ts +13 -0
  32. package/dist/toxics/LatencyToxic.js +40 -0
  33. package/dist/toxics/StatusToxic.d.ts +9 -0
  34. package/dist/toxics/StatusToxic.js +22 -0
  35. package/dist/types.d.ts +123 -0
  36. package/dist/types.js +4 -0
  37. package/package.json +35 -0
@@ -0,0 +1,998 @@
1
+ /**
2
+ * FaultMesh — Minimalist Dashboard Client
3
+ * Plain English, Simple Interactions, Full Response Inspector
4
+ */
5
+
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ // DOM Elements
8
+ const themeToggle = document.getElementById('themeToggle');
9
+ const testEndpointSelect = document.getElementById('testEndpointSelect');
10
+ const btnSendTestRequest = document.getElementById('btnSendTestRequest');
11
+ const testFeedback = document.getElementById('testFeedback');
12
+ const activeRulesIndicator = document.getElementById('activeRulesIndicator');
13
+
14
+ // Response Inspector Elements
15
+ const responseInspector = document.getElementById('responseInspector');
16
+ const respStatusBadge = document.getElementById('respStatusBadge');
17
+ const respDuration = document.getElementById('respDuration');
18
+ const respAppliedRule = document.getElementById('respAppliedRule');
19
+ const responseBodyViewer = document.getElementById('responseBodyViewer');
20
+ const btnCloseResponse = document.getElementById('btnCloseResponse');
21
+
22
+ const btnPresetSlowMobile = document.getElementById('btnPresetSlowMobile');
23
+ const btnPresetDisconnect = document.getElementById('btnPresetDisconnect');
24
+ const btnPresetOutage = document.getElementById('btnPresetOutage');
25
+ const btnPresetCorrupt = document.getElementById('btnPresetCorrupt');
26
+ const btnClearAllRules = document.getElementById('btnClearAllRules');
27
+
28
+ const activeRulesList = document.getElementById('activeRulesList');
29
+ const activeRulesCount = document.getElementById('activeRulesCount');
30
+
31
+ const ruleTypeSelect = document.getElementById('ruleTypeSelect');
32
+ const ruleDirectionSelect = document.getElementById('ruleDirectionSelect');
33
+ const dynamicRuleInputs = document.getElementById('dynamicRuleInputs');
34
+ const customRuleForm = document.getElementById('customRuleForm');
35
+
36
+ const tabResilience = document.getElementById('tabResilience');
37
+ const tabSecurity = document.getElementById('tabSecurity');
38
+ const tabStorm = document.getElementById('tabStorm');
39
+ const diagHeaderTitle = document.getElementById('diagHeaderTitle');
40
+ const diagHeaderSubtitle = document.getElementById('diagHeaderSubtitle');
41
+ const testTargetProfile = document.getElementById('testTargetProfile');
42
+ const btnRunDiagnostics = document.getElementById('btnRunDiagnostics');
43
+ const scoreValue = document.getElementById('scoreValue');
44
+ const scoreGrade = document.getElementById('scoreGrade');
45
+ const scoreTitle = document.getElementById('scoreTitle');
46
+ const scoreSubtitle = document.getElementById('scoreSubtitle');
47
+ const diagnosticsResults = document.getElementById('diagnosticsResults');
48
+
49
+ const activityFeed = document.getElementById('activityFeed');
50
+ const btnClearLog = document.getElementById('btnClearLog');
51
+
52
+ let activeRules = [];
53
+ let isRunningDiagnostics = false;
54
+ let currentDiagMode = 'resilience';
55
+
56
+ // 1. Theme Management (Clean Light / Neutral Dark)
57
+ function initTheme() {
58
+ const saved = localStorage.getItem('faultmesh_theme') || 'light';
59
+ document.documentElement.setAttribute('data-theme', saved);
60
+ }
61
+
62
+ themeToggle.addEventListener('click', () => {
63
+ const current = document.documentElement.getAttribute('data-theme') || 'light';
64
+ const next = current === 'dark' ? 'light' : 'dark';
65
+ document.documentElement.setAttribute('data-theme', next);
66
+ localStorage.setItem('faultmesh_theme', next);
67
+ });
68
+ initTheme();
69
+
70
+ // 2. Response Inspector Panel
71
+ if (btnCloseResponse) {
72
+ btnCloseResponse.addEventListener('click', () => {
73
+ if (respStatusBadge) {
74
+ respStatusBadge.textContent = 'Ready';
75
+ respStatusBadge.className = 'status-pill';
76
+ }
77
+ if (respDuration) respDuration.textContent = '-- ms';
78
+ if (respAppliedRule) respAppliedRule.textContent = 'Rule: None';
79
+ if (testFeedback) {
80
+ testFeedback.textContent = 'Click "Send Test Request" or enable any scenario on the left to inspect responses.';
81
+ testFeedback.style.color = 'var(--text-muted)';
82
+ }
83
+ if (responseBodyViewer) {
84
+ responseBodyViewer.textContent = '(No response received yet. Select an endpoint above or click "Enable & Test" on any scenario on the left.)';
85
+ }
86
+ });
87
+ }
88
+
89
+ function displayResponse(status, statusText, durationMs, appliedRules, bodyText, isError = false) {
90
+ if (!responseInspector) return;
91
+
92
+ respStatusBadge.textContent = status ? `HTTP ${status} ${statusText || ''}` : 'Connection Severed';
93
+ respStatusBadge.className = 'status-pill ' + (isError || status >= 500 ? 'code-5xx' : (status >= 400 ? 'code-4xx' : 'code-2xx'));
94
+ respDuration.textContent = `${durationMs}ms`;
95
+
96
+ const ruleNames = appliedRules && appliedRules.length > 0 ? appliedRules.join(', ') : 'None (Normal Traffic)';
97
+ respAppliedRule.textContent = `Applied: ${ruleNames}`;
98
+
99
+ // Format body as formatted JSON if possible
100
+ try {
101
+ const parsed = JSON.parse(bodyText);
102
+ responseBodyViewer.textContent = JSON.stringify(parsed, null, 2);
103
+ } catch {
104
+ responseBodyViewer.textContent = bodyText || '(Empty Response Body)';
105
+ }
106
+ }
107
+
108
+ // 3. Dynamic Input Fields for Custom Rules
109
+ function renderDynamicInputs() {
110
+ const type = ruleTypeSelect.value;
111
+ let html = '';
112
+
113
+ switch (type) {
114
+ case 'latency':
115
+ html = `
116
+ <div class="form-row">
117
+ <div class="form-field">
118
+ <label>Delay (ms)</label>
119
+ <input type="number" id="inputDelayMs" class="form-input" value="300" min="0" max="60000" step="10" required>
120
+ </div>
121
+ <div class="form-field">
122
+ <label>Random Variation (&plusmn; ms)</label>
123
+ <input type="number" id="inputVarianceMs" class="form-input" value="50" min="0" max="10000" step="10">
124
+ </div>
125
+ </div>
126
+ `;
127
+ break;
128
+ case 'bandwidth':
129
+ html = `
130
+ <div class="form-field">
131
+ <label>Speed Limit (kbps) — 16 kbps is ~2 KB/s</label>
132
+ <input type="number" id="inputSpeedKbps" class="form-input" value="16" min="1" max="50000" step="1" required>
133
+ </div>
134
+ `;
135
+ break;
136
+ case 'cut':
137
+ html = `
138
+ <div class="form-field">
139
+ <label>Drop Connection After (Bytes)</label>
140
+ <input type="number" id="inputCutBytes" class="form-input" value="30" min="1" max="100000" step="1" required>
141
+ </div>
142
+ `;
143
+ break;
144
+ case 'corrupt':
145
+ html = `
146
+ <div class="form-row">
147
+ <div class="form-field">
148
+ <label>Corruption Type</label>
149
+ <select id="inputCorruptMethod" class="form-input">
150
+ <option value="truncate">Cut Ending (Truncated JSON)</option>
151
+ <option value="bitflip">Null-Byte Replacement</option>
152
+ <option value="garbage">Inject Noise Characters</option>
153
+ </select>
154
+ </div>
155
+ <div class="form-field">
156
+ <label>Chance</label>
157
+ <select id="inputCorruptFrequency" class="form-input">
158
+ <option value="1.0">100% of responses</option>
159
+ <option value="0.5">50% of responses</option>
160
+ </select>
161
+ </div>
162
+ </div>
163
+ `;
164
+ break;
165
+ case 'status':
166
+ html = `
167
+ <div class="form-row">
168
+ <div class="form-field">
169
+ <label>HTTP Status Code</label>
170
+ <select id="inputStatusCode" class="form-input">
171
+ <option value="503">503 Service Unavailable</option>
172
+ <option value="500">500 Internal Server Error</option>
173
+ <option value="502">502 Bad Gateway</option>
174
+ <option value="429">429 Too Many Requests</option>
175
+ <option value="504">504 Gateway Timeout</option>
176
+ </select>
177
+ </div>
178
+ <div class="form-field">
179
+ <label>Status Message (Optional)</label>
180
+ <input type="text" id="inputStatusText" class="form-input" placeholder="e.g. Service Unavailable">
181
+ </div>
182
+ </div>
183
+ `;
184
+ break;
185
+ }
186
+ dynamicRuleInputs.innerHTML = html;
187
+ }
188
+
189
+ ruleTypeSelect.addEventListener('change', renderDynamicInputs);
190
+ renderDynamicInputs();
191
+
192
+ // 4. Status Polling & Active Rules Rendering
193
+ async function refreshStatus() {
194
+ try {
195
+ const res = await fetch('/_faultmesh/status');
196
+ const data = await res.json();
197
+ renderActiveRules(data.activeRules || data.activeToxics || []);
198
+ } catch (err) {
199
+ console.warn('Status fetch error:', err);
200
+ }
201
+ }
202
+
203
+ function renderActiveRules(rules) {
204
+ activeRules = rules || [];
205
+ activeRulesCount.textContent = `${activeRules.length} active`;
206
+
207
+ if (activeRulesIndicator) {
208
+ if (activeRules.length === 0) {
209
+ activeRulesIndicator.textContent = 'Traffic: Normal';
210
+ activeRulesIndicator.className = 'active-rules-pill';
211
+ } else {
212
+ const names = activeRules.map(r => r.name).join(', ');
213
+ activeRulesIndicator.textContent = `Simulating: ${names}`;
214
+ activeRulesIndicator.className = 'active-rules-pill active';
215
+ }
216
+ }
217
+
218
+ if (activeRules.length === 0) {
219
+ activeRulesList.innerHTML = '<div class="empty-state">No rules active. All traffic passes through normally.</div>';
220
+ return;
221
+ }
222
+
223
+ activeRulesList.innerHTML = activeRules.map(r => `
224
+ <div class="rule-item">
225
+ <div>
226
+ <div class="rule-title">${escapeHtml(r.name)}</div>
227
+ <div class="rule-subtitle">${formatRuleDetail(r)}</div>
228
+ </div>
229
+ <div style="display: flex; gap: 6px; align-items: center;">
230
+ <button class="btn-secondary btn-sm" onclick="testActiveRule()" title="Send test request through proxy">Test Rule</button>
231
+ <button class="btn-remove btn-sm" onclick="deleteRule('${r.id}')">Remove</button>
232
+ </div>
233
+ </div>
234
+ `).join('');
235
+ }
236
+
237
+ function formatRuleDetail(r) {
238
+ const c = r.config || {};
239
+ const dir = r.direction === 'downstream' ? 'Responses' : 'Requests';
240
+ switch (r.type) {
241
+ case 'latency': return `${dir} delayed +${c.latencyMs}ms (${c.jitterMs ? `±${c.jitterMs}ms` : 'fixed'})`;
242
+ case 'bandwidth': return `${dir} capped at ${c.rateKbps} kbps`;
243
+ case 'cut': return `Socket dropped after ${c.cutAfterBytes} bytes`;
244
+ case 'corrupt': return `Corrupted response (${c.corruptType || 'truncate'})`;
245
+ case 'status': return `Returning HTTP ${c.statusCode}`;
246
+ default: return JSON.stringify(c);
247
+ }
248
+ }
249
+
250
+ window.deleteRule = async function(id) {
251
+ try {
252
+ await fetch(`/_faultmesh/rules/${id}`, { method: 'DELETE' });
253
+ refreshStatus();
254
+ } catch (err) {
255
+ console.error('Delete rule error:', err);
256
+ }
257
+ };
258
+
259
+ btnClearAllRules.addEventListener('click', async () => {
260
+ try {
261
+ await fetch('/_faultmesh/rules', { method: 'DELETE' });
262
+ await refreshStatus();
263
+ executeTestRequest();
264
+ } catch (err) {
265
+ console.error('Clear rules error:', err);
266
+ }
267
+ });
268
+
269
+ // 5. Custom Rule Form Submit
270
+ customRuleForm.addEventListener('submit', async (e) => {
271
+ e.preventDefault();
272
+ const type = ruleTypeSelect.value;
273
+ const direction = ruleDirectionSelect.value;
274
+ const id = `rule_${type}_${Date.now()}`;
275
+ let name = '';
276
+ let config = {};
277
+
278
+ switch (type) {
279
+ case 'latency':
280
+ const latencyMs = Number(document.getElementById('inputDelayMs').value);
281
+ const jitterMs = Number(document.getElementById('inputVarianceMs').value || 0);
282
+ name = `Delay (+${latencyMs}ms)`;
283
+ config = { latencyMs, jitterMs };
284
+ break;
285
+ case 'bandwidth':
286
+ const rateKbps = Number(document.getElementById('inputSpeedKbps').value);
287
+ name = `Speed Cap (${rateKbps} kbps)`;
288
+ config = { rateKbps };
289
+ break;
290
+ case 'cut':
291
+ const cutAfterBytes = Number(document.getElementById('inputCutBytes').value);
292
+ name = `Drop Socket (@ ${cutAfterBytes}B)`;
293
+ config = { cutAfterBytes };
294
+ break;
295
+ case 'corrupt':
296
+ const corruptType = document.getElementById('inputCorruptMethod').value;
297
+ const corruptProbability = Number(document.getElementById('inputCorruptFrequency').value);
298
+ name = `Corrupt Data (${corruptType})`;
299
+ config = { corruptType, corruptProbability };
300
+ break;
301
+ case 'status':
302
+ const statusCode = Number(document.getElementById('inputStatusCode').value);
303
+ const statusMessage = document.getElementById('inputStatusText').value || undefined;
304
+ name = `HTTP ${statusCode}`;
305
+ config = { statusCode, statusMessage };
306
+ break;
307
+ }
308
+
309
+ try {
310
+ await fetch('/_faultmesh/rules', {
311
+ method: 'POST',
312
+ headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ id, name, type, direction, enabled: true, config }),
314
+ });
315
+ await refreshStatus();
316
+ executeTestRequest();
317
+ } catch (err) {
318
+ console.error('Add rule error:', err);
319
+ }
320
+ });
321
+
322
+ // 6. Direct Test Request Execution
323
+ async function executeTestRequest(endpointOverride) {
324
+ btnSendTestRequest.disabled = true;
325
+ const endpoint = endpointOverride || (testEndpointSelect ? testEndpointSelect.value : '/api/data');
326
+ testFeedback.textContent = `Sending ${endpoint} through http://localhost:3001...`;
327
+ testFeedback.style.color = 'var(--text-muted)';
328
+
329
+ const start = Date.now();
330
+ try {
331
+ const res = await fetch(`http://localhost:3001${endpoint}`);
332
+ const duration = Date.now() - start;
333
+ const text = await res.text();
334
+
335
+ const activeNames = activeRules.map(r => r.name);
336
+
337
+ if (res.ok) {
338
+ testFeedback.textContent = `HTTP ${res.status} OK (${duration}ms)`;
339
+ testFeedback.style.color = 'var(--tag-green-fg)';
340
+ displayResponse(res.status, res.statusText, duration, activeNames, text, false);
341
+ } else {
342
+ testFeedback.textContent = `HTTP ${res.status} Error (${duration}ms)`;
343
+ testFeedback.style.color = 'var(--tag-red-fg)';
344
+ displayResponse(res.status, res.statusText, duration, activeNames, text, true);
345
+ }
346
+ } catch (err) {
347
+ const duration = Date.now() - start;
348
+ testFeedback.textContent = `Connection dropped or failed (${duration}ms): ${err.message}`;
349
+ testFeedback.style.color = 'var(--tag-red-fg)';
350
+ displayResponse(0, 'Socket Dropped', duration, activeRules.map(r => r.name), `Network Error: ${err.message}\n\nConnection was abruptly severed or timed out by the FaultMesh proxy rule.`, true);
351
+ } finally {
352
+ btnSendTestRequest.disabled = false;
353
+ refreshStatus();
354
+ }
355
+ }
356
+
357
+ btnSendTestRequest.addEventListener('click', () => executeTestRequest());
358
+ window.testActiveRule = function() {
359
+ executeTestRequest();
360
+ };
361
+
362
+ // 7. Presets (Enable & Immediately Test)
363
+ btnPresetSlowMobile.addEventListener('click', async () => {
364
+ await fetch('/_faultmesh/rules', {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify({
368
+ id: `preset_bw_${Date.now()}`,
369
+ name: 'Slow Mobile Internet (16 kbps limit)',
370
+ type: 'bandwidth',
371
+ direction: 'downstream',
372
+ enabled: true,
373
+ config: { rateKbps: 16 },
374
+ }),
375
+ });
376
+ await fetch('/_faultmesh/rules', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({
380
+ id: `preset_lat_${Date.now()}`,
381
+ name: 'Mobile Delay (350ms)',
382
+ type: 'latency',
383
+ direction: 'downstream',
384
+ enabled: true,
385
+ config: { latencyMs: 350, jitterMs: 50 },
386
+ }),
387
+ });
388
+ await refreshStatus();
389
+ executeTestRequest();
390
+ });
391
+
392
+ btnPresetDisconnect.addEventListener('click', async () => {
393
+ await fetch('/_faultmesh/rules', {
394
+ method: 'POST',
395
+ headers: { 'Content-Type': 'application/json' },
396
+ body: JSON.stringify({
397
+ id: `preset_cut_${Date.now()}`,
398
+ name: 'Connection Drop (after 30 bytes)',
399
+ type: 'cut',
400
+ direction: 'downstream',
401
+ enabled: true,
402
+ config: { cutAfterBytes: 30 },
403
+ }),
404
+ });
405
+ await refreshStatus();
406
+ executeTestRequest();
407
+ });
408
+
409
+ btnPresetOutage.addEventListener('click', async () => {
410
+ await fetch('/_faultmesh/rules', {
411
+ method: 'POST',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ body: JSON.stringify({
414
+ id: `preset_503_${Date.now()}`,
415
+ name: 'Server Outage (HTTP 503)',
416
+ type: 'status',
417
+ direction: 'downstream',
418
+ enabled: true,
419
+ config: { statusCode: 503, statusMessage: 'Service Unavailable' },
420
+ }),
421
+ });
422
+ await refreshStatus();
423
+ executeTestRequest();
424
+ });
425
+
426
+ btnPresetCorrupt.addEventListener('click', async () => {
427
+ await fetch('/_faultmesh/rules', {
428
+ method: 'POST',
429
+ headers: { 'Content-Type': 'application/json' },
430
+ body: JSON.stringify({
431
+ id: `preset_corrupt_${Date.now()}`,
432
+ name: 'Corrupted Response (Truncated JSON)',
433
+ type: 'corrupt',
434
+ direction: 'downstream',
435
+ enabled: true,
436
+ config: { corruptType: 'truncate', corruptProbability: 1.0 },
437
+ }),
438
+ });
439
+ await refreshStatus();
440
+ executeTestRequest();
441
+ });
442
+
443
+ // 8. Diagnostics & Security Audit Suite Definitions
444
+ const RESILIENCE_CHECKS = [
445
+ {
446
+ num: '01',
447
+ name: 'Response Delay Handling (300ms)',
448
+ category: 'Latency',
449
+ description: 'Checks whether the client handles delayed network responses without freezing UI threads or aborting prematurely.',
450
+ probe: 'Injects 250ms latency + 50ms jitter into downstream HTTP traffic.'
451
+ },
452
+ {
453
+ num: '02',
454
+ name: 'Slow Connection Throughput (16 kbps)',
455
+ category: 'Bandwidth',
456
+ description: 'Simulates low-speed mobile connections (~2 KB/s) to evaluate streaming chunk buffering and prevent payload buffer overflows.',
457
+ probe: 'Downstream token-bucket rate limiting at 16 kbps.'
458
+ },
459
+ {
460
+ num: '03',
461
+ name: 'Abrupt Disconnection Mid-Transfer',
462
+ category: 'Connection',
463
+ description: 'Tests if connection drops while receiving data are caught cleanly without unhandled socket reset exceptions (ECONNRESET).',
464
+ probe: 'Abruptly severs downstream TCP socket after 15 bytes transferred.'
465
+ },
466
+ {
467
+ num: '04',
468
+ name: 'Malformed JSON Handling',
469
+ category: 'Integrity',
470
+ description: 'Checks whether the application safely detects broken, truncated, or malformed JSON payloads without uncaught parser crashes.',
471
+ probe: 'Simulates mid-string payload truncation of response bodies.'
472
+ },
473
+ {
474
+ num: '05',
475
+ name: 'HTTP 503 Service Outage Recovery',
476
+ category: 'Availability',
477
+ description: 'Tests if the application properly receives, traps, and gracefully responds to temporary upstream server downtime.',
478
+ probe: 'Forces proxy to return HTTP 503 Service Unavailable.'
479
+ }
480
+ ];
481
+
482
+ const SECURITY_CHECKS = [
483
+ {
484
+ num: '01',
485
+ name: 'Defensive Security Headers',
486
+ category: 'Protocol',
487
+ severity: 'high',
488
+ description: 'Checks for essential defensive response headers: nosniff (MIME sniffing defense), X-Frame-Options (clickjacking defense), and HSTS.',
489
+ probe: 'Passive HTTP response header inspection against API endpoints.'
490
+ },
491
+ {
492
+ num: '02',
493
+ name: 'CORS & Origin Validation',
494
+ category: 'Access Control',
495
+ severity: 'critical',
496
+ description: 'Checks whether Access-Control-Allow-Origin wildcard (*) is combined with credentials or blindly reflects untrusted third-party origins.',
497
+ probe: 'Probes preflight headers with untrusted third-party Origin header.'
498
+ },
499
+ {
500
+ num: '03',
501
+ name: 'URL Credential & Secret Exposure',
502
+ category: 'Data Leakage',
503
+ severity: 'critical',
504
+ description: 'Checks whether authentication tokens, keys, or passwords are passed in GET query strings (?token=...) where they get permanently stored in proxy and server access logs.',
505
+ probe: 'Inspects query parameter ingestion and log isolation.'
506
+ },
507
+ {
508
+ num: '04',
509
+ name: 'Outbound Response PII & Secret Scanner',
510
+ category: 'Data Leakage',
511
+ severity: 'critical',
512
+ description: 'Scans outbound JSON response bodies for leaked internal credentials, private keys, AWS tokens, or bcrypt password hashes.',
513
+ probe: 'Outbound response payload entropy and secret regex scanner.'
514
+ },
515
+ {
516
+ num: '05',
517
+ name: 'Error Sanitization & Stack Trace Exposure',
518
+ category: 'Info Disclosure',
519
+ severity: 'medium',
520
+ description: 'Checks whether 5xx runtime errors return sanitized JSON rather than exposing internal database errors, file paths, or language stack traces.',
521
+ probe: 'Inspects error response bodies for unhandled exception traces.'
522
+ },
523
+ {
524
+ num: '06',
525
+ name: 'Path Traversal & Directory Escape (../)',
526
+ category: 'File Security',
527
+ severity: 'critical',
528
+ description: 'Probes file and path parameters with directory escape sequences (../../etc/passwd) to verify strict path isolation and sanitization.',
529
+ probe: 'Path traversal canary probe verifying clean rejection with HTTP 400.'
530
+ },
531
+ {
532
+ num: '07',
533
+ name: 'Safe SQL/NoSQL & Canary Input Probing',
534
+ category: 'Injection Defense',
535
+ severity: 'critical',
536
+ 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
+ probe: 'Zero-damage canary probe verifying parameter escaping and schema validation.'
538
+ }
539
+ ];
540
+
541
+ const TRAFFIC_STORM_CHECKS = [
542
+ {
543
+ num: '01',
544
+ name: 'Rate Limit Back-off & Retry Storm Handling',
545
+ category: 'Rate Limit',
546
+ severity: 'high',
547
+ description: 'Tests if client respects HTTP 429 Retry-After headers with exponential backoff rather than causing self-inflicted retry stampedes.',
548
+ probe: 'Simulates HTTP 429 response with Retry-After: 2 and observes client retry cadence.'
549
+ },
550
+ {
551
+ num: '02',
552
+ name: 'Oversized Payload & Buffer OOM Defense (HTTP 413)',
553
+ category: 'DoS Defense',
554
+ severity: 'critical',
555
+ description: 'Evaluates if the server rejects oversized request payloads early (HTTP 413) without buffering entire streams into RAM to avoid out-of-memory crashes.',
556
+ probe: 'Streams a chunked payload exceeding the max limit to verify early termination.'
557
+ },
558
+ {
559
+ num: '03',
560
+ name: 'Slowloris Connection Drip Defense',
561
+ category: 'Connection Timeout',
562
+ severity: 'critical',
563
+ description: 'Tests if the server enforces socket read timeouts when requests drip bytes at a very slow rate, preventing socket descriptor exhaustion.',
564
+ probe: 'Drips request bytes at a controlled slow interval to verify read timeout enforcement.'
565
+ },
566
+ {
567
+ num: '04',
568
+ name: 'Duplicate Request Idempotency Protection',
569
+ category: 'Idempotency',
570
+ severity: 'critical',
571
+ description: 'Verifies that concurrent duplicate POST requests sharing an Idempotency-Key are deduplicated to prevent double-billing and duplicate records.',
572
+ probe: 'Dispatches concurrent POST requests with duplicate Idempotency-Key headers.'
573
+ }
574
+ ];
575
+
576
+ function renderPreflightChecklist(mode) {
577
+ let list = RESILIENCE_CHECKS;
578
+ if (mode === 'security') list = SECURITY_CHECKS;
579
+ else if (mode === 'storm') list = TRAFFIC_STORM_CHECKS;
580
+
581
+ const countTag = document.getElementById('checklistCountTag');
582
+ if (countTag) {
583
+ countTag.textContent = `${list.length} checks ready`;
584
+ }
585
+
586
+ diagnosticsResults.innerHTML = list.map(c => `
587
+ <div class="test-row">
588
+ <div class="test-row-main">
589
+ <span class="test-num">${c.num}</span>
590
+ <div class="test-content-col">
591
+ <div class="test-name-group">
592
+ <span class="test-name">${escapeHtml(c.name)}</span>
593
+ <span class="category-pill">${escapeHtml(c.category)}</span>
594
+ ${c.severity ? `<span class="severity-pill severity-${c.severity}">${c.severity}</span>` : ''}
595
+ </div>
596
+ <div class="test-desc">${escapeHtml(c.description)}</div>
597
+ <div class="test-probe-method">Probe Method: ${escapeHtml(c.probe)}</div>
598
+ </div>
599
+ </div>
600
+ <div class="test-row-aside">
601
+ <span class="status-ready">READY</span>
602
+ </div>
603
+ </div>
604
+ `).join('');
605
+ }
606
+
607
+ function setDiagMode(mode) {
608
+ currentDiagMode = mode;
609
+ if (tabResilience) tabResilience.classList.remove('active');
610
+ if (tabSecurity) tabSecurity.classList.remove('active');
611
+ if (tabStorm) tabStorm.classList.remove('active');
612
+
613
+ if (mode === 'storm') {
614
+ if (tabStorm) tabStorm.classList.add('active');
615
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Traffic Storms & DoS Defense Suite';
616
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Evaluates rate limit backoff (429), oversized payload protection (413), slowloris drips, and idempotency deduplication.';
617
+ if (testTargetProfile) {
618
+ testTargetProfile.innerHTML = `
619
+ <option value="resilient">Target: Resilient System (Grade A)</option>
620
+ <option value="fragile">Target: Fragile System (Grade F)</option>
621
+ `;
622
+ }
623
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run Traffic Storm Benchmark';
624
+ scoreTitle.textContent = 'Ready for Traffic Storms & DoS evaluation';
625
+ scoreSubtitle.textContent = 'Click "Run Traffic Storm Benchmark" to test rate limit backoff, slowloris defense, and idempotency.';
626
+ } else if (mode === 'security') {
627
+ if (tabSecurity) tabSecurity.classList.add('active');
628
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Security & Protocol Audit';
629
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Non-destructive 7-point audit evaluating defensive headers, CORS, URL tokens, response PII, path traversal, and canary injection.';
630
+ if (testTargetProfile) {
631
+ testTargetProfile.innerHTML = `
632
+ <option value="secure">Target: Secure API (Grade A)</option>
633
+ <option value="vulnerable">Target: Vulnerable API (Grade F)</option>
634
+ `;
635
+ }
636
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run Security Audit';
637
+ scoreTitle.textContent = 'Ready to audit security';
638
+ scoreSubtitle.textContent = 'Click "Run Security Audit" to evaluate all 7 defensive security, CORS, PII, and injection probes.';
639
+ } else {
640
+ if (tabResilience) tabResilience.classList.add('active');
641
+ if (diagHeaderTitle) diagHeaderTitle.textContent = 'Automated API Resilience Benchmark';
642
+ if (diagHeaderSubtitle) diagHeaderSubtitle.textContent = 'Automated 5-point test suite evaluating client resilience against network delay, throttling, cuts, corruptions, and 503 outages.';
643
+ if (testTargetProfile) {
644
+ testTargetProfile.innerHTML = `
645
+ <option value="resilient">Target: Resilient App (Grade A)</option>
646
+ <option value="fragile">Target: Fragile App (Grade F)</option>
647
+ `;
648
+ }
649
+ if (btnRunDiagnostics) btnRunDiagnostics.textContent = 'Run 5-Point Benchmark';
650
+ scoreTitle.textContent = 'Ready to evaluate resilience';
651
+ scoreSubtitle.textContent = 'Click "Run 5-Point Benchmark" to evaluate client resilience against 5 real failure cases.';
652
+ }
653
+ scoreValue.textContent = '--';
654
+ scoreGrade.textContent = 'UNTESTED';
655
+ renderPreflightChecklist(mode);
656
+ }
657
+
658
+ if (tabResilience) tabResilience.addEventListener('click', () => setDiagMode('resilience'));
659
+ if (tabSecurity) tabSecurity.addEventListener('click', () => setDiagMode('security'));
660
+ if (tabStorm) tabStorm.addEventListener('click', () => setDiagMode('storm'));
661
+
662
+ if (testTargetProfile) {
663
+ testTargetProfile.addEventListener('change', () => {
664
+ scoreValue.textContent = '--';
665
+ scoreGrade.textContent = 'UNTESTED';
666
+ const label = testTargetProfile.options[testTargetProfile.selectedIndex]?.text || '';
667
+ scoreTitle.textContent = 'Target Profile Updated';
668
+ scoreSubtitle.textContent = `Selected: ${label}. Click run to execute benchmark.`;
669
+ renderPreflightChecklist(currentDiagMode);
670
+ });
671
+ }
672
+
673
+ btnRunDiagnostics.addEventListener('click', async () => {
674
+ if (isRunningDiagnostics) return;
675
+ isRunningDiagnostics = true;
676
+ btnRunDiagnostics.disabled = true;
677
+
678
+ if (currentDiagMode === 'storm') {
679
+ btnRunDiagnostics.textContent = 'Auditing...';
680
+ scoreTitle.textContent = 'Running Traffic Storm Benchmark...';
681
+ scoreSubtitle.textContent = 'Auditing HTTP 429 rate limit backoff, oversized payloads (413), slowloris drips, and duplicate POST idempotency.';
682
+
683
+ const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
684
+
685
+ try {
686
+ const res = await fetch(`/_faultmesh/storm/run?profile=${encodeURIComponent(profile)}`, {
687
+ method: 'POST',
688
+ headers: { 'Content-Type': 'application/json' },
689
+ body: JSON.stringify({ profile }),
690
+ });
691
+ const report = await res.json();
692
+ renderTrafficStormReport(report);
693
+ refreshStatus();
694
+ } catch (err) {
695
+ console.error('Traffic storm error:', err);
696
+ scoreTitle.textContent = 'Traffic Storm Audit Failed';
697
+ scoreSubtitle.textContent = err.message;
698
+ } finally {
699
+ isRunningDiagnostics = false;
700
+ btnRunDiagnostics.disabled = false;
701
+ btnRunDiagnostics.textContent = 'Run Traffic Storm Benchmark';
702
+ }
703
+ } else if (currentDiagMode === 'security') {
704
+ btnRunDiagnostics.textContent = 'Auditing...';
705
+ scoreTitle.textContent = 'Running Security Audit...';
706
+ scoreSubtitle.textContent = 'Auditing defensive headers, CORS safety, query secret leakage, response PII, and inert canary probes.';
707
+
708
+ const profile = testTargetProfile ? testTargetProfile.value : 'secure';
709
+
710
+ try {
711
+ const res = await fetch(`/_faultmesh/security/run?profile=${encodeURIComponent(profile)}`, {
712
+ method: 'POST',
713
+ headers: { 'Content-Type': 'application/json' },
714
+ body: JSON.stringify({ profile }),
715
+ });
716
+ const report = await res.json();
717
+ renderSecurityReport(report);
718
+ refreshStatus();
719
+ } catch (err) {
720
+ console.error('Security audit error:', err);
721
+ scoreTitle.textContent = 'Security Audit Failed';
722
+ scoreSubtitle.textContent = err.message;
723
+ } finally {
724
+ isRunningDiagnostics = false;
725
+ btnRunDiagnostics.disabled = false;
726
+ btnRunDiagnostics.textContent = 'Run Security Audit';
727
+ }
728
+ } else {
729
+ btnRunDiagnostics.textContent = 'Running...';
730
+ scoreTitle.textContent = 'Running 5-Point Benchmark...';
731
+ scoreSubtitle.textContent = 'Evaluating delay, slow speed, connection drops, corrupted JSON, and 503 errors.';
732
+
733
+ const profile = testTargetProfile ? testTargetProfile.value : 'resilient';
734
+
735
+ try {
736
+ const res = await fetch(`/_faultmesh/diagnostics/run?profile=${encodeURIComponent(profile)}`, {
737
+ method: 'POST',
738
+ headers: { 'Content-Type': 'application/json' },
739
+ body: JSON.stringify({ profile }),
740
+ });
741
+ const report = await res.json();
742
+ renderDiagnosticsReport(report);
743
+ refreshStatus();
744
+ } catch (err) {
745
+ console.error('Diagnostics error:', err);
746
+ scoreTitle.textContent = 'Diagnostics Failed';
747
+ scoreSubtitle.textContent = err.message;
748
+ } finally {
749
+ isRunningDiagnostics = false;
750
+ btnRunDiagnostics.disabled = false;
751
+ btnRunDiagnostics.textContent = 'Run 5-Point Benchmark';
752
+ }
753
+ }
754
+ });
755
+
756
+ function renderDiagnosticsReport(report) {
757
+ scoreValue.textContent = `${report.score}/100`;
758
+ scoreGrade.textContent = `GRADE ${report.grade}`;
759
+
760
+ if (report.grade === 'A') {
761
+ scoreTitle.textContent = 'All 5 Resilience Checks Passed (Grade A)';
762
+ } else if (report.grade === 'B') {
763
+ scoreTitle.textContent = 'Passed with Minor Issues (Grade B)';
764
+ } else {
765
+ scoreTitle.textContent = 'Resilience Failures Detected (Grade ' + report.grade + ')';
766
+ }
767
+
768
+ scoreSubtitle.textContent = `Passed ${report.passedAttacks} of ${report.totalAttacks} failure checks.`;
769
+
770
+ const countTag = document.getElementById('checklistCountTag');
771
+ if (countTag) {
772
+ countTag.textContent = `${report.passedAttacks}/${report.totalAttacks} passed`;
773
+ }
774
+
775
+ diagnosticsResults.innerHTML = report.results.map((r, idx) => {
776
+ const checkMeta = RESILIENCE_CHECKS[idx] || {};
777
+ const num = String(idx + 1).padStart(2, '0');
778
+ const rowClass = r.passed ? 'row-passed' : 'row-failed';
779
+ const findingClass = r.passed ? 'finding-pass' : 'finding-fail';
780
+
781
+ return `
782
+ <div class="test-row ${rowClass}">
783
+ <div class="test-row-main">
784
+ <span class="test-num">${num}</span>
785
+ <div class="test-content-col">
786
+ <div class="test-name-group">
787
+ <span class="test-name">${escapeHtml(r.name)}</span>
788
+ ${checkMeta.category ? `<span class="category-pill">${escapeHtml(checkMeta.category)}</span>` : ''}
789
+ </div>
790
+ <div class="test-desc">${escapeHtml(r.description)}</div>
791
+ <div class="test-finding-box ${findingClass}">
792
+ Finding: ${escapeHtml(r.details)}
793
+ </div>
794
+ </div>
795
+ </div>
796
+ <div class="test-row-aside">
797
+ <span class="test-tag ${r.passed ? 'tag-pass' : 'tag-fail'}">
798
+ ${r.passed ? 'PASSED' : 'FAILED'}
799
+ </span>
800
+ <span style="font-family:var(--font-mono); font-size:11px; color:var(--text-muted);">${r.latencyMs}ms</span>
801
+ </div>
802
+ </div>
803
+ `;
804
+ }).join('') + `
805
+ <div class="advice-list">
806
+ <strong>Resilience Recommendations:</strong>
807
+ <ul>
808
+ ${report.recommendations.map(rec => `<li>${escapeHtml(rec)}</li>`).join('')}
809
+ </ul>
810
+ </div>
811
+ `;
812
+ }
813
+
814
+ function renderSecurityReport(report) {
815
+ scoreValue.textContent = `${report.score}/100`;
816
+ scoreGrade.textContent = `GRADE ${report.grade}`;
817
+
818
+ if (report.grade === 'A') {
819
+ scoreTitle.textContent = 'All 7 Security Checks Passed (Grade A)';
820
+ } else if (report.grade === 'B') {
821
+ scoreTitle.textContent = 'Minor Security Issues Detected (Grade B)';
822
+ } else {
823
+ scoreTitle.textContent = 'Security Vulnerabilities Detected (Grade ' + report.grade + ')';
824
+ }
825
+
826
+ scoreSubtitle.textContent = `Passed ${report.passedChecks} of ${report.totalChecks} security checks.`;
827
+
828
+ const countTag = document.getElementById('checklistCountTag');
829
+ if (countTag) {
830
+ countTag.textContent = `${report.passedChecks}/${report.totalChecks} passed`;
831
+ }
832
+
833
+ diagnosticsResults.innerHTML = report.checks.map((c, idx) => {
834
+ const num = String(idx + 1).padStart(2, '0');
835
+ const rowClass = c.passed ? 'row-passed' : 'row-failed';
836
+ const findingClass = c.passed ? 'finding-pass' : 'finding-fail';
837
+
838
+ return `
839
+ <div class="test-row ${rowClass}">
840
+ <div class="test-row-main">
841
+ <span class="test-num">${num}</span>
842
+ <div class="test-content-col">
843
+ <div class="test-name-group">
844
+ <span class="test-name">${escapeHtml(c.name)}</span>
845
+ <span class="category-pill">${escapeHtml(c.category)}</span>
846
+ <span class="severity-pill severity-${c.severity}">${c.severity}</span>
847
+ </div>
848
+ <div class="test-desc">${escapeHtml(c.description)}</div>
849
+ <div class="test-finding-box ${findingClass}">
850
+ Finding: ${escapeHtml(c.details)}
851
+ </div>
852
+ </div>
853
+ </div>
854
+ <div class="test-row-aside">
855
+ <span class="test-tag ${c.passed ? 'tag-pass' : 'tag-fail'}">
856
+ ${c.passed ? 'PASSED' : 'FAILED'}
857
+ </span>
858
+ <span style="font-family:var(--font-mono); font-size:11px; color:var(--text-muted);">${c.latencyMs}ms</span>
859
+ </div>
860
+ </div>
861
+ `;
862
+ }).join('') + `
863
+ <div class="advice-list">
864
+ <strong>Remediation Recommendations:</strong>
865
+ <ul>
866
+ ${report.recommendations.map(rec => `<li>${escapeHtml(rec)}</li>`).join('')}
867
+ </ul>
868
+ </div>
869
+ `;
870
+ }
871
+
872
+ function renderTrafficStormReport(report) {
873
+ scoreValue.textContent = `${report.score}/100`;
874
+ scoreGrade.textContent = `GRADE ${report.grade}`;
875
+
876
+ if (report.grade === 'A') {
877
+ scoreTitle.textContent = 'All 4 Traffic Storm Checks Passed (Grade A)';
878
+ } else if (report.grade === 'B') {
879
+ scoreTitle.textContent = 'Passed with Minor Issues (Grade B)';
880
+ } else {
881
+ scoreTitle.textContent = 'Traffic Storm & DoS Failures Detected (Grade ' + report.grade + ')';
882
+ }
883
+
884
+ scoreSubtitle.textContent = `Passed ${report.passedChecks} of ${report.totalChecks} storm and DoS defense checks.`;
885
+
886
+ const countTag = document.getElementById('checklistCountTag');
887
+ if (countTag) {
888
+ countTag.textContent = `${report.passedChecks}/${report.totalChecks} passed`;
889
+ }
890
+
891
+ diagnosticsResults.innerHTML = report.checks.map((c, idx) => {
892
+ const num = String(idx + 1).padStart(2, '0');
893
+ const rowClass = c.passed ? 'row-passed' : 'row-failed';
894
+ const findingClass = c.passed ? 'finding-pass' : 'finding-fail';
895
+
896
+ return `
897
+ <div class="test-row ${rowClass}">
898
+ <div class="test-row-main">
899
+ <span class="test-num">${num}</span>
900
+ <div class="test-content-col">
901
+ <div class="test-name-group">
902
+ <span class="test-name">${escapeHtml(c.name)}</span>
903
+ <span class="category-pill">${escapeHtml(c.category)}</span>
904
+ <span class="severity-pill severity-${c.severity}">${c.severity}</span>
905
+ </div>
906
+ <div class="test-desc">${escapeHtml(c.description)}</div>
907
+ <div class="test-finding-box ${findingClass}">
908
+ Finding: ${escapeHtml(c.details)}
909
+ </div>
910
+ </div>
911
+ </div>
912
+ <div class="test-row-aside">
913
+ <span class="test-tag ${c.passed ? 'tag-pass' : 'tag-fail'}">
914
+ ${c.passed ? 'PASSED' : 'FAILED'}
915
+ </span>
916
+ <span style="font-family:var(--font-mono); font-size:11px; color:var(--text-muted);">${c.latencyMs}ms</span>
917
+ </div>
918
+ </div>
919
+ `;
920
+ }).join('') + `
921
+ <div class="advice-list">
922
+ <strong>Traffic Storm & DoS Recommendations:</strong>
923
+ <ul>
924
+ ${report.recommendations.map(rec => `<li>${escapeHtml(rec)}</li>`).join('')}
925
+ </ul>
926
+ </div>
927
+ `;
928
+ }
929
+
930
+ // 9. Live Request SSE Stream
931
+ function setupSSE() {
932
+ const eventSource = new EventSource('/_faultmesh/telemetry/stream');
933
+
934
+ eventSource.addEventListener('snapshot', (e) => {
935
+ try {
936
+ const data = JSON.parse(e.data);
937
+ if (data.events && data.events.length > 0) {
938
+ activityFeed.innerHTML = '';
939
+ data.events.forEach(addLogRow);
940
+ }
941
+ } catch (err) {
942
+ console.warn('Snapshot parse error', err);
943
+ }
944
+ });
945
+
946
+ eventSource.addEventListener('telemetry', (e) => {
947
+ try {
948
+ const event = JSON.parse(e.data);
949
+ addLogRow(event);
950
+ refreshStatus();
951
+ } catch (err) {
952
+ console.warn('Telemetry parse error', err);
953
+ }
954
+ });
955
+ }
956
+
957
+ function addLogRow(ev) {
958
+ if (activityFeed.querySelector('.table-empty')) {
959
+ activityFeed.innerHTML = '';
960
+ }
961
+
962
+ const tr = document.createElement('tr');
963
+ let codeClass = 'code-2xx';
964
+ if (ev.statusCode >= 500) codeClass = 'code-5xx';
965
+ else if (ev.statusCode >= 400) codeClass = 'code-4xx';
966
+
967
+ const ruleText = ev.appliedToxics && ev.appliedToxics.length > 0
968
+ ? ev.appliedToxics.join(', ')
969
+ : 'None';
970
+
971
+ tr.innerHTML = `
972
+ <td style="font-weight:600;">${ev.method}</td>
973
+ <td>${escapeHtml(ev.path)}</td>
974
+ <td class="${codeClass}">${ev.statusCode}</td>
975
+ <td style="color:var(--text-muted);">${escapeHtml(ruleText)}</td>
976
+ <td style="text-align:right; color:var(--text-muted); font-variant-numeric:tabular-nums;">${ev.durationMs}ms</td>
977
+ `;
978
+
979
+ activityFeed.prepend(tr);
980
+ while (activityFeed.children.length > 30) {
981
+ activityFeed.removeChild(activityFeed.lastChild);
982
+ }
983
+ }
984
+
985
+ btnClearLog.addEventListener('click', () => {
986
+ activityFeed.innerHTML = '<tr><td colspan="5" class="table-empty">Log cleared. Listening for requests...</td></tr>';
987
+ });
988
+
989
+ function escapeHtml(str) {
990
+ return String(str || '').replace(/[&<>"']/g, m => ({
991
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
992
+ }[m]));
993
+ }
994
+
995
+ setDiagMode('resilience');
996
+ refreshStatus();
997
+ setupSSE();
998
+ });