โ— Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / server / live / dashboard.html
โ† Back to directory
2677 lines ยท 95.1 KB
Raw
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>CONTRABAND // Command Center Live Telemetry</title>
7 <style>
8 :root {
9 --bg: #0a0a0c;
10 --card-bg: #121218;
11 --card-hover: #181924;
12 --border: #1a2234;
13 --border-bright: #2b3954;
14 --cyan: #06b6d4;
15 --cyan-glow: rgba(6, 182, 212, 0.4);
16 --blue: #0ea5e9;
17 --blue-glow: rgba(14, 165, 233, 0.35);
18 --crimson: #ef4444;
19 --crimson-glow: rgba(239, 68, 68, 0.4);
20 --amber: #f59e0b;
21 --amber-glow: rgba(245, 158, 11, 0.35);
22 --emerald: #10b981;
23 --emerald-glow: rgba(16, 185, 129, 0.4);
24 --green: #10b981;
25 --green-glow: rgba(16, 185, 129, 0.4);
26 --text: #e2e8f0;
27 --text-dim: #718096;
28 --text-bright: #ffffff;
29 --mono-font: 'JetBrains Mono', 'SF Mono', Consolas, Monaco, monospace;
30 --sans-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
31 }
32
33 * { box-sizing: border-box; margin: 0; padding: 0; }
34 body {
35 background: var(--bg);
36 color: var(--text);
37 font-family: var(--sans-font);
38 min-height: 100vh;
39 overflow-x: hidden;
40 line-height: 1.5;
41 }
42
43 /* Top Nav */
44 header {
45 display: flex;
46 justify-content: space-between;
47 align-items: center;
48 padding: 16px 28px;
49 background: rgba(14, 17, 26, 0.85);
50 backdrop-filter: blur(12px);
51 border-bottom: 1px solid var(--border);
52 position: sticky;
53 top: 0;
54 z-index: 100;
55 }
56 .brand {
57 display: flex;
58 align-items: center;
59 gap: 14px;
60 }
61 .logo-badge {
62 background: linear-gradient(135deg, var(--cyan), var(--blue));
63 color: #000;
64 font-weight: 900;
65 font-size: 13px;
66 letter-spacing: 2px;
67 padding: 6px 12px;
68 border-radius: 4px;
69 box-shadow: 0 0 16px var(--cyan-glow);
70 text-transform: uppercase;
71 }
72 .brand h1 {
73 font-size: 18px;
74 font-weight: 800;
75 letter-spacing: 1.5px;
76 color: var(--text-bright);
77 }
78 .brand h1 span {
79 color: var(--cyan);
80 }
81 .nav-actions {
82 display: flex;
83 align-items: center;
84 gap: 16px;
85 }
86 .feed-status {
87 display: flex;
88 align-items: center;
89 gap: 8px;
90 font-family: var(--mono-font);
91 font-size: 12px;
92 background: rgba(0, 255, 102, 0.1);
93 border: 1px solid var(--green);
94 color: var(--green);
95 padding: 4px 12px;
96 border-radius: 20px;
97 }
98 .feed-pulse {
99 width: 8px;
100 height: 8px;
101 background: var(--green);
102 border-radius: 50%;
103 box-shadow: 0 0 8px var(--green);
104 animation: pulse 1.5s infinite;
105 }
106 @keyframes pulse {
107 0%, 100% { opacity: 1; transform: scale(1); }
108 50% { opacity: 0.4; transform: scale(0.8); }
109 }
110 .nav-btn {
111 background: var(--card-bg);
112 border: 1px solid var(--border-bright);
113 color: var(--text);
114 padding: 6px 14px;
115 border-radius: 6px;
116 font-size: 12px;
117 font-weight: 600;
118 cursor: pointer;
119 transition: all 0.2s;
120 text-decoration: none;
121 }
122 .nav-btn:hover {
123 background: var(--border);
124 border-color: var(--cyan);
125 color: var(--cyan);
126 }
127 .btn-danger {
128 background: rgba(239, 68, 68, 0.12) !important;
129 border-color: rgba(239, 68, 68, 0.4) !important;
130 color: var(--crimson) !important;
131 }
132 .btn-danger:hover {
133 background: rgba(239, 68, 68, 0.25) !important;
134 border-color: var(--crimson) !important;
135 color: #fff !important;
136 box-shadow: 0 0 10px var(--crimson-glow);
137 }
138
139 /* Container */
140 .container {
141 width: 100%;
142 max-width: 100%;
143 margin: 0;
144 padding: 16px 24px;
145 display: flex;
146 flex-direction: column;
147 gap: 20px;
148 box-sizing: border-box;
149 }
150
151 /* Radial Dials Row */
152 .dials-grid {
153 display: grid;
154 grid-template-columns: repeat(4, 1fr);
155 gap: 20px;
156 }
157 @media (max-width: 1200px) {
158 .dials-grid { grid-template-columns: repeat(2, 1fr); }
159 }
160 @media (max-width: 640px) {
161 .dials-grid { grid-template-columns: 1fr; }
162 }
163
164 .dial-card {
165 background: var(--card-bg);
166 border: 1px solid var(--border);
167 border-radius: 12px;
168 padding: 20px;
169 display: flex;
170 flex-direction: column;
171 align-items: center;
172 position: relative;
173 overflow: hidden;
174 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
175 transition: transform 0.2s, border-color 0.2s;
176 }
177 .dial-card:hover {
178 transform: translateY(-2px);
179 border-color: var(--border-bright);
180 }
181 .dial-card::before {
182 content: '';
183 position: absolute;
184 top: 0;
185 left: 0;
186 right: 0;
187 height: 2px;
188 background: linear-gradient(90deg, transparent, var(--dial-accent, var(--cyan)), transparent);
189 }
190 .dial-title {
191 font-size: 12px;
192 text-transform: uppercase;
193 letter-spacing: 1.5px;
194 color: var(--text-dim);
195 font-weight: 700;
196 margin-bottom: 12px;
197 }
198 .gauge-wrapper {
199 position: relative;
200 width: 170px;
201 height: 170px;
202 }
203 .gauge-svg {
204 width: 100%;
205 height: 100%;
206 transform: rotate(-90deg);
207 }
208 .gauge-bg {
209 fill: none;
210 stroke: rgba(255, 255, 255, 0.06);
211 stroke-width: 12;
212 }
213 .gauge-progress {
214 fill: none;
215 stroke-width: 12;
216 stroke-linecap: round;
217 stroke-dasharray: 440;
218 stroke-dashoffset: 440;
219 transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
220 }
221 .gauge-center {
222 position: absolute;
223 top: 50%;
224 left: 50%;
225 transform: translate(-50%, -50%);
226 text-align: center;
227 }
228 .gauge-val {
229 font-family: var(--mono-font);
230 font-size: 26px;
231 font-weight: 800;
232 color: var(--text-bright);
233 line-height: 1;
234 }
235 .gauge-unit {
236 font-size: 11px;
237 color: var(--text-dim);
238 font-family: var(--mono-font);
239 margin-top: 4px;
240 text-transform: uppercase;
241 }
242 .dial-meta {
243 margin-top: 14px;
244 font-size: 12px;
245 font-family: var(--mono-font);
246 color: var(--text-dim);
247 }
248
249 /* Grid Sections */
250 .matrices-grid {
251 display: grid;
252 grid-template-columns: 1fr 1fr;
253 gap: 24px;
254 }
255 @media (max-width: 1100px) {
256 .matrices-grid { grid-template-columns: 1fr; }
257 }
258
259 .main-grid {
260 display: grid;
261 grid-template-columns: 1fr 1fr;
262 gap: 24px;
263 }
264 @media (max-width: 1100px) {
265 .main-grid { grid-template-columns: 1fr; }
266 }
267
268 .section-card {
269 background: var(--card-bg);
270 border: 1px solid var(--border);
271 border-radius: 12px;
272 padding: 20px;
273 display: flex;
274 flex-direction: column;
275 gap: 16px;
276 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
277 }
278 .section-header {
279 display: flex;
280 justify-content: space-between;
281 align-items: center;
282 border-bottom: 1px solid var(--border);
283 padding-bottom: 12px;
284 }
285 .section-title {
286 font-size: 14px;
287 text-transform: uppercase;
288 letter-spacing: 1.2px;
289 font-weight: 800;
290 color: var(--text-bright);
291 display: flex;
292 align-items: center;
293 gap: 10px;
294 }
295 .section-title svg {
296 width: 18px;
297 height: 18px;
298 fill: var(--cyan);
299 }
300 .badge-count {
301 background: rgba(0, 255, 204, 0.1);
302 color: var(--cyan);
303 font-family: var(--mono-font);
304 font-size: 11px;
305 padding: 2px 8px;
306 border-radius: 10px;
307 border: 1px solid rgba(0, 255, 204, 0.3);
308 }
309
310 /* Active Streams Table / Feed */
311 .stream-feed {
312 display: flex;
313 flex-direction: column;
314 gap: 8px;
315 max-height: 420px;
316 overflow-y: auto;
317 }
318 .stream-row {
319 background: rgba(255, 255, 255, 0.02);
320 border: 1px solid var(--border);
321 border-radius: 8px;
322 padding: 10px 16px;
323 display: grid;
324 grid-template-columns: minmax(130px, 180px) minmax(160px, 240px) 120px 1fr 100px 130px;
325 align-items: center;
326 gap: 14px;
327 transition: background 0.15s, border-color 0.15s;
328 }
329 .stream-row-active {
330 border-color: rgba(0, 255, 102, 0.4);
331 background: rgba(0, 255, 102, 0.03);
332 box-shadow: 0 0 12px rgba(0, 255, 102, 0.08);
333 }
334 .stream-row-recent {
335 border-color: rgba(255, 255, 255, 0.06);
336 }
337 .stream-row:hover {
338 background: rgba(255, 255, 255, 0.04);
339 border-color: var(--border-bright);
340 }
341 @media (max-width: 1024px) {
342 .stream-row {
343 grid-template-columns: 1fr 1fr;
344 gap: 8px;
345 }
346 }
347 .host-info {
348 font-family: var(--mono-font);
349 font-size: 12px;
350 font-weight: 700;
351 color: var(--text-bright);
352 overflow: hidden;
353 text-overflow: ellipsis;
354 white-space: nowrap;
355 }
356 .zone-pill {
357 font-family: var(--mono-font);
358 font-size: 10px;
359 font-weight: 700;
360 text-transform: uppercase;
361 padding: 3px 8px;
362 border-radius: 4px;
363 display: inline-block;
364 text-align: center;
365 background: rgba(0, 168, 255, 0.15);
366 color: var(--blue);
367 border: 1px solid rgba(0, 168, 255, 0.4);
368 white-space: nowrap;
369 overflow: hidden;
370 text-overflow: ellipsis;
371 max-width: 100%;
372 }
373 .proto-pill {
374 font-family: var(--mono-font);
375 font-size: 10px;
376 font-weight: 700;
377 text-transform: uppercase;
378 padding: 3px 8px;
379 border-radius: 4px;
380 display: inline-block;
381 text-align: center;
382 background: rgba(168, 85, 247, 0.15);
383 color: #c084fc;
384 border: 1px solid rgba(168, 85, 247, 0.4);
385 }
386 .progress-bar-container {
387 display: flex;
388 flex-direction: column;
389 gap: 4px;
390 }
391 .progress-track {
392 background: rgba(255, 255, 255, 0.05);
393 border-radius: 4px;
394 height: 6px;
395 overflow: hidden;
396 position: relative;
397 }
398 .progress-fill {
399 background: linear-gradient(90deg, var(--cyan), var(--blue));
400 height: 100%;
401 border-radius: 4px;
402 transition: width 0.3s ease;
403 box-shadow: 0 0 8px var(--cyan-glow);
404 }
405 .progress-fill.fill-success {
406 background: linear-gradient(90deg, var(--green), var(--emerald));
407 box-shadow: 0 0 8px var(--emerald-glow);
408 }
409 .progress-fill.fill-blocked {
410 background: linear-gradient(90deg, var(--crimson), #f43f5e);
411 box-shadow: 0 0 8px var(--crimson-glow);
412 }
413 .progress-labels {
414 display: flex;
415 justify-content: space-between;
416 font-family: var(--mono-font);
417 font-size: 10px;
418 color: var(--text-dim);
419 }
420 .rate-val {
421 font-family: var(--mono-font);
422 font-size: 12px;
423 font-weight: 700;
424 color: var(--cyan);
425 text-align: right;
426 }
427 .status-badge {
428 font-family: var(--mono-font);
429 font-size: 10px;
430 font-weight: 700;
431 padding: 3px 8px;
432 border-radius: 4px;
433 text-align: center;
434 white-space: nowrap;
435 }
436 .status-streaming {
437 background: rgba(0, 255, 102, 0.15);
438 color: var(--green);
439 border: 1px solid var(--green);
440 animation: pulse 2s infinite;
441 }
442 .status-success {
443 background: rgba(16, 185, 129, 0.15);
444 color: var(--green);
445 border: 1px solid var(--green);
446 }
447 .status-blocked {
448 background: rgba(239, 68, 68, 0.15);
449 color: var(--crimson);
450 border: 1px solid var(--crimson);
451 }
452 .status-idle {
453 background: rgba(255, 255, 255, 0.05);
454 color: var(--text-dim);
455 border: 1px solid var(--border);
456 }
457 .empty-state {
458 padding: 40px;
459 text-align: center;
460 color: var(--text-dim);
461 font-family: var(--mono-font);
462 font-size: 13px;
463 }
464
465 /* 20-Protocol Smuggling Matrix Grid */
466 .protocol-grid {
467 display: grid;
468 grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
469 gap: 12px;
470 }
471 .proto-card {
472 background: rgba(255, 255, 255, 0.02);
473 border: 1px solid var(--border);
474 border-radius: 8px;
475 padding: 12px;
476 display: flex;
477 flex-direction: column;
478 gap: 6px;
479 position: relative;
480 transition: all 0.2s;
481 }
482 .proto-card:hover {
483 border-color: var(--border-bright);
484 background: rgba(255, 255, 255, 0.04);
485 }
486 .proto-card.active-stream {
487 border-color: var(--green);
488 box-shadow: 0 0 12px var(--green-glow);
489 background: rgba(0, 255, 102, 0.04);
490 }
491 .proto-card.blocked-state {
492 border-color: var(--crimson);
493 background: rgba(255, 0, 85, 0.04);
494 }
495 .proto-header {
496 display: flex;
497 justify-content: space-between;
498 align-items: center;
499 }
500 .proto-name {
501 font-family: var(--mono-font);
502 font-size: 11px;
503 font-weight: 800;
504 color: var(--text-bright);
505 text-transform: uppercase;
506 }
507 .proto-beacon {
508 width: 8px;
509 height: 8px;
510 border-radius: 50%;
511 background: var(--text-dim);
512 }
513 .beacon-ready { background: var(--cyan); box-shadow: 0 0 6px var(--cyan); }
514 .beacon-streaming { background: var(--green); box-shadow: 0 0 8px var(--green); animation: pulse 1.2s infinite; }
515 .beacon-blocked { background: var(--crimson); box-shadow: 0 0 6px var(--crimson); }
516 .proto-vol {
517 font-family: var(--mono-font);
518 font-size: 11px;
519 color: var(--text-dim);
520 }
521
522 /* Port Status Matrix // Rack View */
523 .ports-grid {
524 display: grid;
525 grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
526 gap: 10px;
527 }
528 .port-card {
529 background: rgba(255, 255, 255, 0.02);
530 border: 1px solid var(--border);
531 border-radius: 6px;
532 padding: 10px;
533 display: flex;
534 flex-direction: column;
535 gap: 4px;
536 transition: all 0.2s;
537 }
538 .port-card:hover {
539 border-color: var(--border-bright);
540 background: rgba(255, 255, 255, 0.04);
541 }
542 .port-card.status-receiving {
543 border-color: var(--cyan);
544 box-shadow: 0 0 10px var(--cyan-glow);
545 background: rgba(6, 182, 212, 0.05);
546 }
547 .port-card.status-reflection {
548 border-color: var(--amber);
549 box-shadow: 0 0 10px var(--amber-glow);
550 background: rgba(245, 158, 11, 0.05);
551 }
552 .port-card.status-blocked {
553 border-color: var(--crimson);
554 box-shadow: 0 0 8px var(--crimson-glow);
555 background: rgba(239, 68, 68, 0.05);
556 }
557 .port-card.status-listening {
558 border-color: rgba(16, 185, 129, 0.4);
559 }
560 .port-header {
561 display: flex;
562 justify-content: space-between;
563 align-items: center;
564 }
565 .port-num {
566 font-family: var(--mono-font);
567 font-size: 13px;
568 font-weight: 800;
569 color: var(--text-bright);
570 }
571 .port-beacon {
572 width: 7px;
573 height: 7px;
574 border-radius: 50%;
575 }
576 .beacon-listening {
577 background: var(--emerald);
578 box-shadow: 0 0 6px var(--emerald-glow);
579 }
580 .beacon-receiving {
581 background: var(--cyan);
582 box-shadow: 0 0 10px var(--cyan-glow);
583 animation: pulse 0.8s infinite;
584 }
585 .beacon-reflection {
586 background: var(--amber);
587 box-shadow: 0 0 10px var(--amber-glow);
588 animation: pulse 1.0s infinite;
589 }
590 .beacon-port-blocked {
591 background: var(--crimson);
592 box-shadow: 0 0 6px var(--crimson-glow);
593 }
594 .beacon-idle {
595 background: var(--text-dim);
596 }
597 .port-meta {
598 font-family: var(--mono-font);
599 font-size: 10px;
600 color: var(--text-dim);
601 display: flex;
602 justify-content: space-between;
603 }
604
605 /* Live Terminal Console */
606 #liveEventStreamSection {
607 transition: all 0.25s ease;
608 }
609 #liveEventStreamSection.full-bleed {
610 margin-left: -24px;
611 margin-right: -24px;
612 border-radius: 0;
613 border-left: none;
614 border-right: none;
615 width: calc(100% + 48px);
616 box-shadow: 0 0 32px rgba(0, 0, 0, 0.85);
617 }
618 .terminal-card {
619 background: #050608;
620 border: 1px solid var(--border);
621 border-radius: 8px;
622 display: flex;
623 flex-direction: column;
624 height: 480px;
625 overflow: hidden;
626 font-family: var(--mono-font);
627 box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7);
628 transition: height 0.25s ease, width 0.25s ease;
629 }
630 .terminal-card.expanded {
631 height: 720px;
632 }
633 .terminal-card.compact {
634 height: 340px;
635 }
636 .terminal-card.fullscreen {
637 position: fixed;
638 top: 10px;
639 left: 10px;
640 right: 10px;
641 bottom: 10px;
642 height: calc(100vh - 20px) !important;
643 width: calc(100vw - 20px) !important;
644 z-index: 99999;
645 border: 1px solid var(--cyan);
646 box-shadow: 0 0 60px rgba(0, 0, 0, 0.95), 0 0 25px var(--cyan-glow);
647 }
648 body.terminal-fullscreen-active {
649 overflow: hidden;
650 }
651 .terminal-header {
652 background: rgba(255, 255, 255, 0.03);
653 border-bottom: 1px solid var(--border);
654 padding: 8px 16px;
655 display: flex;
656 justify-content: space-between;
657 align-items: center;
658 font-size: 11px;
659 color: var(--text-dim);
660 }
661 .terminal-controls {
662 display: flex;
663 align-items: center;
664 gap: 10px;
665 flex-wrap: wrap;
666 }
667 .filter-pills {
668 display: flex;
669 gap: 4px;
670 background: rgba(255, 255, 255, 0.03);
671 padding: 2px 4px;
672 border-radius: 6px;
673 border: 1px solid var(--border);
674 }
675 .filter-pill {
676 background: transparent;
677 border: none;
678 color: var(--text-dim);
679 font-family: var(--mono-font);
680 font-size: 10.5px;
681 font-weight: 700;
682 padding: 3px 8px;
683 border-radius: 4px;
684 cursor: pointer;
685 transition: all 0.15s;
686 }
687 .filter-pill:hover {
688 color: var(--text);
689 background: rgba(255, 255, 255, 0.06);
690 }
691 .filter-pill.active {
692 color: #000;
693 background: var(--cyan);
694 box-shadow: 0 0 8px var(--cyan-glow);
695 }
696 .filter-pill.pill-alert.active {
697 background: var(--crimson);
698 color: #fff;
699 box-shadow: 0 0 8px var(--crimson-glow);
700 }
701 .filter-pill.pill-success.active {
702 background: var(--green);
703 color: #000;
704 box-shadow: 0 0 8px var(--green-glow);
705 }
706 .filter-pill.pill-exfil.active {
707 background: var(--blue);
708 color: #fff;
709 box-shadow: 0 0 8px var(--blue-glow);
710 }
711 .filter-pill.pill-info.active {
712 background: var(--text-dim);
713 color: #fff;
714 }
715 .auto-scroll-label {
716 display: flex;
717 align-items: center;
718 gap: 6px;
719 font-family: var(--mono-font);
720 font-size: 11px;
721 color: var(--text-dim);
722 cursor: pointer;
723 user-select: none;
724 }
725 .auto-scroll-label input {
726 cursor: pointer;
727 accent-color: var(--cyan);
728 }
729 .terminal-logs {
730 padding: 12px 16px;
731 overflow-y: auto;
732 overflow-x: auto;
733 flex: 1;
734 display: flex;
735 flex-direction: column;
736 gap: 4px;
737 font-size: 12px;
738 line-height: 1.5;
739 }
740 .terminal-logs.nowrap .log-line {
741 white-space: nowrap;
742 }
743 .log-line {
744 display: flex;
745 align-items: baseline;
746 gap: 10px;
747 padding: 3px 8px;
748 border-radius: 4px;
749 transition: background 0.1s;
750 white-space: pre-wrap;
751 }
752 .log-line:hover {
753 background: rgba(255, 255, 255, 0.04);
754 }
755 .log-time {
756 color: var(--text-dim);
757 flex-shrink: 0;
758 font-size: 11px;
759 font-variant-numeric: tabular-nums;
760 }
761 .log-lvl {
762 font-weight: 800;
763 font-size: 10px;
764 letter-spacing: 0.5px;
765 padding: 1px 7px;
766 border-radius: 4px;
767 flex-shrink: 0;
768 text-transform: uppercase;
769 }
770 .lvl-INFO { color: var(--blue); background: rgba(14, 165, 233, 0.12); border: 1px solid rgba(14, 165, 233, 0.25); }
771 .lvl-SUCCESS { color: var(--green); background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.25); }
772 .lvl-WARN { color: var(--amber); background: rgba(245, 158, 11, 0.12); border: 1px solid rgba(245, 158, 11, 0.25); }
773 .lvl-ALERT { color: var(--crimson); background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); }
774 .lvl-EXFIL { color: var(--cyan); background: rgba(6, 182, 212, 0.12); border: 1px solid rgba(6, 182, 212, 0.25); }
775 .log-zone {
776 color: #c084fc;
777 background: rgba(168, 85, 247, 0.1);
778 border: 1px solid rgba(168, 85, 247, 0.25);
779 padding: 1px 6px;
780 border-radius: 3px;
781 font-size: 11px;
782 flex-shrink: 0;
783 white-space: nowrap;
784 }
785 .log-proto {
786 color: var(--cyan);
787 background: rgba(6, 182, 212, 0.1);
788 border: 1px solid rgba(6, 182, 212, 0.25);
789 padding: 1px 6px;
790 border-radius: 3px;
791 font-size: 11px;
792 flex-shrink: 0;
793 white-space: nowrap;
794 text-transform: uppercase;
795 }
796 .log-msg {
797 color: var(--text);
798 flex: 1;
799 overflow-wrap: break-word;
800 word-break: break-word;
801 }
802
803 /* Reports Drawer / Modal */
804 .reports-table {
805 width: 100%;
806 border-collapse: collapse;
807 font-size: 12px;
808 }
809 .reports-table th {
810 text-align: left;
811 padding: 10px 14px;
812 color: var(--text-dim);
813 font-family: var(--mono-font);
814 font-size: 11px;
815 border-bottom: 1px solid var(--border);
816 text-transform: uppercase;
817 }
818 .reports-table td {
819 padding: 12px 14px;
820 border-bottom: 1px solid rgba(255, 255, 255, 0.03);
821 }
822 .report-actions {
823 display: flex;
824 gap: 8px;
825 }
826 .action-link {
827 color: var(--cyan);
828 text-decoration: none;
829 font-family: var(--mono-font);
830 font-size: 11px;
831 padding: 3px 8px;
832 border: 1px solid rgba(0, 255, 204, 0.3);
833 border-radius: 4px;
834 background: rgba(0, 255, 204, 0.05);
835 transition: all 0.2s;
836 }
837 .action-link:hover {
838 background: rgba(0, 255, 204, 0.2);
839 border-color: var(--cyan);
840 }
841
842 /* Modal for previewing HTML reports */
843 .modal-overlay {
844 display: none;
845 position: fixed;
846 top: 0; left: 0; right: 0; bottom: 0;
847 background: rgba(0, 0, 0, 0.85);
848 backdrop-filter: blur(8px);
849 z-index: 999;
850 justify-content: center;
851 align-items: center;
852 padding: 24px;
853 }
854 .modal-content {
855 background: var(--card-bg);
856 border: 1px solid var(--border-bright);
857 border-radius: 12px;
858 width: 95vw;
859 height: 90vh;
860 display: flex;
861 flex-direction: column;
862 overflow: hidden;
863 box-shadow: 0 16px 48px rgba(0, 0, 0, 0.8);
864 }
865 .modal-header {
866 display: flex;
867 justify-content: space-between;
868 align-items: center;
869 padding: 14px 20px;
870 border-bottom: 1px solid var(--border);
871 background: rgba(255, 255, 255, 0.02);
872 }
873 .modal-iframe {
874 flex: 1;
875 border: none;
876 background: #08090c;
877 }
878 .modal-card {
879 background: var(--card-bg);
880 border: 1px solid var(--border-bright);
881 border-radius: 12px;
882 width: 100%;
883 max-width: 520px;
884 display: flex;
885 flex-direction: column;
886 overflow: hidden;
887 box-shadow: 0 16px 48px rgba(0, 0, 0, 0.95), 0 0 24px rgba(239, 68, 68, 0.25);
888 }
889 .modal-body {
890 padding: 20px;
891 }
892 .modal-footer {
893 display: flex;
894 justify-content: flex-end;
895 gap: 10px;
896 padding: 14px 20px;
897 border-top: 1px solid var(--border);
898 background: rgba(255, 255, 255, 0.02);
899 }
900 .btn-cyan {
901 background: rgba(6, 182, 212, 0.12) !important;
902 border-color: rgba(6, 182, 212, 0.45) !important;
903 color: var(--cyan) !important;
904 }
905 .btn-cyan:hover {
906 background: rgba(6, 182, 212, 0.25) !important;
907 border-color: var(--cyan) !important;
908 box-shadow: 0 0 14px var(--cyan-glow);
909 }
910 .modal-card-lg {
911 background: var(--card-bg);
912 border: 1px solid var(--border-bright);
913 border-radius: 12px;
914 width: 100%;
915 max-width: 920px;
916 max-height: 90vh;
917 display: flex;
918 flex-direction: column;
919 overflow: hidden;
920 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.95), 0 0 32px rgba(6, 182, 212, 0.2);
921 }
922 .modal-body-scroll {
923 padding: 22px;
924 overflow-y: auto;
925 display: flex;
926 flex-direction: column;
927 gap: 18px;
928 }
929 .gen-section-title {
930 font-size: 11px;
931 font-weight: 800;
932 color: var(--cyan);
933 letter-spacing: 0.06em;
934 text-transform: uppercase;
935 display: flex;
936 align-items: center;
937 gap: 8px;
938 margin-bottom: 2px;
939 }
940 .gen-grid {
941 display: grid;
942 grid-template-columns: 1fr 1fr;
943 gap: 16px;
944 }
945 @media (max-width: 768px) {
946 .gen-grid { grid-template-columns: 1fr; }
947 }
948 .gen-field {
949 display: flex;
950 flex-direction: column;
951 gap: 5px;
952 }
953 .gen-label {
954 font-size: 11px;
955 font-weight: 700;
956 color: var(--text-dim);
957 text-transform: uppercase;
958 letter-spacing: 0.04em;
959 }
960 .gen-input, .gen-select {
961 background: rgba(8, 11, 18, 0.85);
962 border: 1px solid var(--border);
963 border-radius: 6px;
964 color: var(--text-bright);
965 font-family: var(--mono-font);
966 font-size: 12px;
967 padding: 7px 11px;
968 outline: none;
969 transition: all 0.2s;
970 }
971 .gen-input:focus, .gen-select:focus {
972 border-color: var(--cyan);
973 box-shadow: 0 0 10px var(--cyan-glow);
974 }
975 .gen-pills {
976 display: flex;
977 flex-wrap: wrap;
978 gap: 6px;
979 }
980 .gen-pill {
981 background: rgba(255, 255, 255, 0.04);
982 border: 1px solid var(--border);
983 color: var(--text);
984 padding: 4px 9px;
985 border-radius: 4px;
986 font-size: 11px;
987 font-family: var(--mono-font);
988 cursor: pointer;
989 user-select: none;
990 transition: all 0.15s;
991 }
992 .gen-pill:hover {
993 border-color: var(--cyan);
994 color: var(--cyan);
995 }
996 .gen-pill.active {
997 background: rgba(6, 182, 212, 0.18);
998 border-color: var(--cyan);
999 color: var(--cyan);
1000 font-weight: 700;
1001 box-shadow: 0 0 8px var(--cyan-glow);
1002 }
1003 .gen-cmd-box {
1004 background: #05070c;
1005 border: 1px solid var(--border-bright);
1006 border-radius: 8px;
1007 padding: 14px 16px;
1008 position: relative;
1009 }
1010 .gen-cmd-head {
1011 display: flex;
1012 justify-content: space-between;
1013 align-items: center;
1014 margin-bottom: 8px;
1015 font-size: 11px;
1016 font-family: var(--mono-font);
1017 color: var(--text-dim);
1018 }
1019 .gen-cmd-text {
1020 font-family: var(--mono-font);
1021 font-size: 12px;
1022 color: #38bdf8;
1023 line-height: 1.6;
1024 word-break: break-all;
1025 white-space: pre-wrap;
1026 user-select: all;
1027 }
1028 .gen-checkbox-row {
1029 display: flex;
1030 flex-wrap: wrap;
1031 gap: 14px;
1032 font-size: 12px;
1033 font-family: var(--mono-font);
1034 }
1035 .gen-checkbox-label {
1036 display: flex;
1037 align-items: center;
1038 gap: 8px;
1039 cursor: pointer;
1040 color: var(--text-bright);
1041 user-select: none;
1042 }
1043 .gen-checkbox-label input[type="checkbox"] {
1044 accent-color: var(--cyan);
1045 width: 15px;
1046 height: 15px;
1047 cursor: pointer;
1048 }
1049 </style>
1050</head>
1051<body>
1052
1053 <header>
1054 <div class="brand">
1055 <div class="logo-badge">CTB//01</div>
1056 <h1>CONTRABAND <span>COMMAND CENTER</span></h1>
1057 </div>
1058 <div class="nav-actions">
1059 <span id="liveClock" style="font-family: var(--mono-font); font-size: 13px; color: var(--text-dim); background: var(--card-bg); border: 1px solid var(--border); padding: 4px 10px; border-radius: 6px;">--:--:-- UTC</span>
1060 <div class="feed-status" id="connStatus">
1061 <div class="feed-pulse" id="connPulse"></div>
1062 <span id="connText">STREAM CONNECTED</span>
1063 </div>
1064 <button class="nav-btn btn-cyan" onclick="openCmdGenModal()" style="display: flex; align-items: center; gap: 6px;">
1065 <span style="font-size: 13px;">โšก</span> CLIENT CLI GENERATOR
1066 </button>
1067 <button class="nav-btn" onclick="scrollToReports()">VIEW SAVED REPORTS</button>
1068 <button class="nav-btn btn-danger" onclick="openResetModal()">RESET DASHBOARD</button>
1069 </div>
1070 </header>
1071
1072 <div class="container">
1073
1074 <!-- Radial Dials Section -->
1075 <div class="dials-grid">
1076 <!-- Dial 1: Instantaneous Throughput -->
1077 <div class="dial-card" style="--dial-accent: var(--cyan);">
1078 <div class="dial-title">Instant Throughput</div>
1079 <div class="gauge-wrapper">
1080 <svg class="gauge-svg" viewBox="0 0 160 160">
1081 <circle class="gauge-bg" cx="80" cy="80" r="65"></circle>
1082 <circle class="gauge-progress" id="dialThroughputProg" cx="80" cy="80" r="65" stroke="var(--cyan)" style="filter: drop-shadow(0 0 8px var(--cyan-glow));"></circle>
1083 </svg>
1084 <div class="gauge-center">
1085 <div class="gauge-val" id="dialThroughputVal">0.00</div>
1086 <div class="gauge-unit">MB/S</div>
1087 </div>
1088 </div>
1089 <div class="dial-meta" id="dialPeakThroughput">Peak: 0.00 MB/s</div>
1090 </div>
1091
1092 <!-- Dial 2: Total Smuggled Data -->
1093 <div class="dial-card" style="--dial-accent: var(--blue);">
1094 <div class="dial-title">Total Smuggled Data</div>
1095 <div class="gauge-wrapper">
1096 <svg class="gauge-svg" viewBox="0 0 160 160">
1097 <circle class="gauge-bg" cx="80" cy="80" r="65"></circle>
1098 <circle class="gauge-progress" id="dialVolumeProg" cx="80" cy="80" r="65" stroke="var(--blue)" style="filter: drop-shadow(0 0 8px var(--blue-glow));"></circle>
1099 </svg>
1100 <div class="gauge-center">
1101 <div class="gauge-val" id="dialVolumeVal" style="font-size: 22px;">0 B</div>
1102 <div class="gauge-unit">VOLUME</div>
1103 </div>
1104 </div>
1105 <div class="dial-meta" id="dialTotalChunks">0 Chunks Assembled</div>
1106 </div>
1107
1108 <!-- Dial 3: Global EEI Exposure Score -->
1109 <div class="dial-card" style="--dial-accent: var(--green);" id="cardEEI">
1110 <div class="dial-title">EEI Exposure Score</div>
1111 <div class="gauge-wrapper">
1112 <svg class="gauge-svg" viewBox="0 0 160 160">
1113 <circle class="gauge-bg" cx="80" cy="80" r="65"></circle>
1114 <circle class="gauge-progress" id="dialEEIProg" cx="80" cy="80" r="65" stroke="var(--green)" style="filter: drop-shadow(0 0 8px var(--green-glow));"></circle>
1115 </svg>
1116 <div class="gauge-center">
1117 <div class="gauge-val" id="dialEEIVal">0%</div>
1118 <div class="gauge-unit" id="dialEEIPosture">HARDENED</div>
1119 </div>
1120 </div>
1121 <div class="dial-meta" id="dialEEIMeta">Egress Posture Rating</div>
1122 </div>
1123
1124 <!-- Dial 4: Active Concurrency -->
1125 <div class="dial-card" style="--dial-accent: #a855f7;">
1126 <div class="dial-title">Active Streams</div>
1127 <div class="gauge-wrapper">
1128 <svg class="gauge-svg" viewBox="0 0 160 160">
1129 <circle class="gauge-bg" cx="80" cy="80" r="65"></circle>
1130 <circle class="gauge-progress" id="dialConcProg" cx="80" cy="80" r="65" stroke="#a855f7" style="filter: drop-shadow(0 0 8px rgba(168, 85, 247, 0.4));"></circle>
1131 </svg>
1132 <div class="gauge-center">
1133 <div class="gauge-val" id="dialConcVal">0</div>
1134 <div class="gauge-unit">CONCURRENT</div>
1135 </div>
1136 </div>
1137 <div class="dial-meta" id="dialTotalSessions">0 Total Sessions</div>
1138 </div>
1139 </div>
1140
1141 <!-- Active Exfiltration Streams & Zone Feeds -->
1142 <div class="section-card">
1143 <div class="section-header">
1144 <div class="section-title">
1145 <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
1146 Multi-Zone Real-Time Exfiltration Streams
1147 </div>
1148 <div class="badge-count" id="activeStreamsBadge">0 Active</div>
1149 </div>
1150 <div class="stream-feed" id="activeStreamsFeed">
1151 <div class="empty-state">No active exfiltration streams currently transmitting. Standby for client triggers...</div>
1152 </div>
1153 </div>
1154
1155 <!-- Live Event Stream Console (Full Width Dedicated Section) -->
1156 <div class="section-card" id="liveEventStreamSection">
1157 <div class="section-header">
1158 <div class="section-title">
1159 <svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10zm-2-1h-4v-2h4v2zm-7.7-4.3L6.7 12l3.6-3.7 1.4 1.4-2.3 2.3 2.3 2.3-1.4 1.4z"/></svg>
1160 Live Event Stream
1161 <span class="badge-count" id="logCountBadge">0 Events</span>
1162 </div>
1163 <div class="terminal-controls">
1164 <div class="filter-pills" id="logFilterGroup">
1165 <button class="filter-pill active" id="filterBtnALL" onclick="setLogFilter('ALL')">ALL</button>
1166 <button class="filter-pill pill-alert" id="filterBtnALERT" onclick="setLogFilter('ALERT')">ALERTS</button>
1167 <button class="filter-pill pill-success" id="filterBtnSUCCESS" onclick="setLogFilter('SUCCESS')">SUCCESS</button>
1168 <button class="filter-pill pill-exfil" id="filterBtnEXFIL" onclick="setLogFilter('EXFIL')">EXFIL</button>
1169 <button class="filter-pill pill-info" id="filterBtnINFO" onclick="setLogFilter('INFO')">INFO</button>
1170 </div>
1171 <label class="auto-scroll-label">
1172 <input type="checkbox" id="autoScrollToggle" checked onchange="toggleAutoScroll(this.checked)">
1173 <span>Auto-scroll</span>
1174 </label>
1175 <button class="nav-btn" id="wrapToggleBtn" style="padding: 4px 10px; font-size: 11px;" onclick="toggleLogWrap()" title="Toggle text wrap">NOWRAP</button>
1176 <button class="nav-btn" id="wideToggleBtn" style="padding: 4px 10px; font-size: 11px;" onclick="toggleLogWide()" title="Expand terminal window horizontally edge-to-edge">โ†” WIDE</button>
1177 <button class="nav-btn" id="expandToggleBtn" style="padding: 4px 10px; font-size: 11px;" onclick="toggleLogExpand()" title="Toggle vertical height (480px / 720px)">โ†• TALL</button>
1178 <button class="nav-btn" id="fullscreenToggleBtn" style="padding: 4px 10px; font-size: 11px;" onclick="toggleLogFullscreen()" title="Maximize terminal to full screen (ESC to exit)">โ›ถ MAXIMIZE</button>
1179 <button class="nav-btn" style="padding: 4px 10px; font-size: 11px;" onclick="clearLogs()">CLEAR</button>
1180 </div>
1181 </div>
1182 <div class="terminal-card" id="terminalCard">
1183 <div class="terminal-header">
1184 <div style="display: flex; align-items: center; gap: 8px;">
1185 <div class="feed-pulse" style="width: 6px; height: 6px;"></div>
1186 <span>TERMINAL LOG (HH:MM:SS.mmm) // REAL-TIME SSE FEED</span>
1187 </div>
1188 <span style="color: var(--cyan); letter-spacing: 1px;">SSE // LIVE</span>
1189 </div>
1190 <div class="terminal-logs" id="terminalLogs">
1191 <!-- Dynamically populated -->
1192 </div>
1193 </div>
1194 </div>
1195
1196 <!-- Telemetry Matrices Grid: 20-Protocol Smuggling Matrix & Monitored Ports Rack View -->
1197 <div class="matrices-grid">
1198 <!-- 20 Protocol Smuggling Matrix -->
1199 <div class="section-card">
1200 <div class="section-header">
1201 <div class="section-title">
1202 <svg viewBox="0 0 24 24"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg>
1203 20-Protocol Smuggling Matrix
1204 </div>
1205 <div class="badge-count">20 Listeners</div>
1206 </div>
1207 <div class="protocol-grid" id="protocolGrid">
1208 <!-- Dynamically populated -->
1209 </div>
1210 </div>
1211
1212 <!-- Port Status Matrix // Multi-Zone Rack Telemetry -->
1213 <div class="section-card">
1214 <div class="section-header">
1215 <div class="section-title">
1216 <svg viewBox="0 0 24 24"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg>
1217 Port Status Matrix // Multi-Zone Rack Telemetry
1218 </div>
1219 <div class="badge-count" id="portsCountBadge">20 Monitored Ports</div>
1220 </div>
1221 <div class="ports-grid" id="portsGrid">
1222 <!-- Dynamically populated -->
1223 </div>
1224 </div>
1225 </div>
1226
1227 <!-- Synchronized Assessment Reports -->
1228 <div class="section-card" id="reportsSection">
1229 <div class="section-header">
1230 <div class="section-title">
1231 <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
1232 Synchronized Client Assessment Reports
1233 </div>
1234 <div class="badge-count" id="reportsCountBadge">0 Stored</div>
1235 </div>
1236 <div style="overflow-x: auto;">
1237 <table class="reports-table">
1238 <thead>
1239 <tr>
1240 <th>Report ID</th>
1241 <th>Target Host</th>
1242 <th>Network Zone</th>
1243 <th>Client Node</th>
1244 <th>EEI Score</th>
1245 <th>Probes</th>
1246 <th>Uploaded</th>
1247 <th>Actions</th>
1248 </tr>
1249 </thead>
1250 <tbody id="reportsTableBody">
1251 <tr>
1252 <td colspan="8" style="text-align: center; color: var(--text-dim); padding: 24px;">No client reports synchronized yet. Run client with <code>--sync-report &lt;dashboard-url&gt;</code> to upload findings.</td>
1253 </tr>
1254 </tbody>
1255 </table>
1256 </div>
1257 </div>
1258
1259 </div>
1260
1261 <!-- Modal for Report Previews -->
1262 <div class="modal-overlay" id="reportModal" onclick="closeReportModal(event)">
1263 <div class="modal-content" onclick="event.stopPropagation()">
1264 <div class="modal-header">
1265 <div style="font-weight: 800; font-family: var(--mono-font); font-size: 14px;" id="modalReportTitle">ASSESSMENT REPORT PREVIEW</div>
1266 <button class="nav-btn" onclick="closeReportModal()">CLOSE</button>
1267 </div>
1268 <iframe class="modal-iframe" id="reportIframe"></iframe>
1269 </div>
1270 </div>
1271
1272 <!-- Modal for Dashboard Reset Confirmation -->
1273 <div class="modal-overlay" id="resetModal" onclick="closeResetModal(event)">
1274 <div class="modal-card" onclick="event.stopPropagation()">
1275 <div class="modal-header">
1276 <div style="font-weight: 800; font-family: var(--mono-font); font-size: 14px; color: var(--crimson); letter-spacing: 0.5px;">RESET DASHBOARD & CLEAR TELEMETRY</div>
1277 <button class="nav-btn" onclick="closeResetModal()">โœ•</button>
1278 </div>
1279 <div class="modal-body" style="display: flex; flex-direction: column; gap: 16px; padding: 20px;">
1280 <p style="color: var(--text-bright); line-height: 1.5; font-size: 13px; margin: 0;">
1281 Are you sure you want to reset the live telemetry dashboard? This will zero all throughput dials, clear in-flight streams, wipe terminal logs, and reset protocol/port matrix counters.
1282 </p>
1283 <label style="display: flex; align-items: flex-start; gap: 12px; cursor: pointer; padding: 12px 14px; background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 6px; user-select: none;">
1284 <input type="checkbox" id="resetDeleteFiles" style="margin-top: 3px; accent-color: var(--crimson); width: 16px; height: 16px; cursor: pointer;" />
1285 <span style="font-size: 12px; font-family: var(--mono-font); color: var(--text-bright); line-height: 1.4;">
1286 <strong style="color: var(--crimson);">Delete exfiltrated evidence files from disk (/evidence)</strong>
1287 <br>
1288 <span style="color: var(--text-dim); font-size: 11px;">Permanently purge physical reconstructed binary payloads and PCAPs from the server evidence storage directory.</span>
1289 </span>
1290 </label>
1291 <div id="resetFeedback" style="font-size: 12px; font-family: var(--mono-font); min-height: 18px;"></div>
1292 </div>
1293 <div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; padding: 14px 20px; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.02);">
1294 <button class="nav-btn" onclick="closeResetModal()">CANCEL</button>
1295 <button class="nav-btn btn-danger" id="confirmResetBtn" onclick="executeReset()">CONFIRM RESET</button>
1296 </div>
1297 </div>
1298 </div>
1299
1300 <!-- Modal for Contraband Client Command Generator -->
1301 <div class="modal-overlay" id="cmdGenModal" onclick="closeCmdGenModal(event)">
1302 <div class="modal-card-lg" onclick="event.stopPropagation()">
1303 <div class="modal-header">
1304 <div style="display: flex; align-items: center; gap: 10px;">
1305 <span style="color: var(--cyan); font-size: 16px;">โšก</span>
1306 <div>
1307 <div style="font-weight: 800; font-family: var(--mono-font); font-size: 14px; color: var(--text-bright); letter-spacing: 0.5px;">CLIENT CLI COMMAND GENERATOR</div>
1308 <div style="font-size: 11px; color: var(--text-dim); font-family: var(--sans-font);">Tailored command builder for automated network egress assessment and data smuggling</div>
1309 </div>
1310 </div>
1311 <button class="nav-btn" onclick="closeCmdGenModal()">โœ•</button>
1312 </div>
1313
1314 <div class="modal-body-scroll">
1315 <!-- Assessment Presets -->
1316 <div>
1317 <div class="gen-section-title"><span>โœฆ</span> Operational Presets</div>
1318 <div class="gen-pills" id="cgPresets">
1319 <div class="gen-pill active" onclick="applyCgPreset('full')">๐Ÿš€ Full Assessment</div>
1320 <div class="gen-pill" onclick="applyCgPreset('quic-ws')">โšก High-Speed Smuggle (QUIC + WS)</div>
1321 <div class="gen-pill" onclick="applyCgPreset('stealth')">๐Ÿฅท Low &amp; Slow Covert (DNS+ICMP)</div>
1322 <div class="gen-pill" onclick="applyCgPreset('proxy')">๐Ÿข Proxy &amp; NTLM Traversal</div>
1323 <div class="gen-pill" onclick="applyCgPreset('smb')">๐Ÿ“ฆ Enterprise SMB Reflection</div>
1324 <div class="gen-pill" onclick="applyCgPreset('dlp')">๐Ÿ”’ DLP Exfiltration Validation</div>
1325 </div>
1326 </div>
1327
1328 <!-- Target & Platform Grid -->
1329 <div class="gen-grid">
1330 <div class="gen-field">
1331 <div class="gen-label">Binary Platform</div>
1332 <select class="gen-select" id="cgPlatform" onchange="updateGeneratedCommand()">
1333 <option value="./bin/contraband client">./bin/contraband client (Standard Pure-Go)</option>
1334 <option value="./bin/dist/contraband-client-linux-amd64">./bin/dist/contraband-client-linux-amd64 (Linux x86_64)</option>
1335 <option value="./bin/dist/contraband-client-linux-arm64">./bin/dist/contraband-client-linux-arm64 (Linux ARM64)</option>
1336 <option value=".\bin\dist\contraband-client-windows-amd64.exe">.\bin\dist\contraband-client-windows-amd64.exe (Windows x64)</option>
1337 <option value="./bin/dist/contraband-client-darwin-arm64">./bin/dist/contraband-client-darwin-arm64 (macOS Apple Silicon)</option>
1338 <option value="docker run --rm -it shehzade/contraband client">docker run --rm -it shehzade/contraband client</option>
1339 </select>
1340 </div>
1341 <div class="gen-field">
1342 <div class="gen-label">Target Server IP / Host</div>
1343 <input class="gen-input" type="text" id="cgServer" placeholder="127.0.0.1 or server IP" value="127.0.0.1" oninput="updateGeneratedCommand()" />
1344 </div>
1345 </div>
1346
1347 <!-- Protocols & Ports Grid -->
1348 <div class="gen-grid">
1349 <div class="gen-field">
1350 <div class="gen-label" style="display: flex; justify-content: space-between;">
1351 <span>Protocols</span>
1352 <span style="font-size: 10px; color: var(--cyan); cursor: pointer;" onclick="selectAllProtocols()">Select All</span>
1353 </div>
1354 <input class="gen-input" type="text" id="cgProtocols" placeholder="all or comma-separated list" value="all" oninput="syncProtoPillsFromInput(); updateGeneratedCommand();" />
1355 <div class="gen-pills" id="cgProtoPills" style="margin-top: 4px;">
1356 <div class="gen-pill" data-proto="http1" onclick="toggleProtoPill('http1')">HTTP/1.1</div>
1357 <div class="gen-pill" data-proto="http2" onclick="toggleProtoPill('http2')">HTTP/2</div>
1358 <div class="gen-pill" data-proto="http3" onclick="toggleProtoPill('http3')">HTTP/3 (QUIC)</div>
1359 <div class="gen-pill" data-proto="websocket" onclick="toggleProtoPill('websocket')">WebSocket</div>
1360 <div class="gen-pill" data-proto="grpc" onclick="toggleProtoPill('grpc')">gRPC</div>
1361 <div class="gen-pill" data-proto="dns_a" onclick="toggleProtoPill('dns_a')">DNS A</div>
1362 <div class="gen-pill" data-proto="dns_txt" onclick="toggleProtoPill('dns_txt')">DNS TXT</div>
1363 <div class="gen-pill" data-proto="doh" onclick="toggleProtoPill('doh')">DoH</div>
1364 <div class="gen-pill" data-proto="ftp" onclick="toggleProtoPill('ftp')">FTP</div>
1365 <div class="gen-pill" data-proto="sftp" onclick="toggleProtoPill('sftp')">SFTP</div>
1366 <div class="gen-pill" data-proto="smtp" onclick="toggleProtoPill('smtp')">SMTP</div>
1367 <div class="gen-pill" data-proto="smb_file" onclick="toggleProtoPill('smb_file')">SMB File</div>
1368 <div class="gen-pill" data-proto="smb_hash" onclick="toggleProtoPill('smb_hash')">SMB Hash</div>
1369 <div class="gen-pill" data-proto="smb_quic" onclick="toggleProtoPill('smb_quic')">SMB QUIC</div>
1370 <div class="gen-pill" data-proto="icmp" onclick="toggleProtoPill('icmp')">ICMP</div>
1371 <div class="gen-pill" data-proto="ntp" onclick="toggleProtoPill('ntp')">NTP</div>
1372 <div class="gen-pill" data-proto="syslog" onclick="toggleProtoPill('syslog')">Syslog</div>
1373 <div class="gen-pill" data-proto="mqtt" onclick="toggleProtoPill('mqtt')">MQTT</div>
1374 </div>
1375 </div>
1376 <div class="gen-field">
1377 <div class="gen-label">Egress Ports</div>
1378 <input class="gen-input" type="text" id="cgPorts" placeholder="quick, standard, full, or 80,443" value="quick" oninput="updateGeneratedCommand()" />
1379 <div class="gen-pills" style="margin-top: 4px;">
1380 <div class="gen-pill" onclick="setCgPort('quick')">Quick (25 Ports)</div>
1381 <div class="gen-pill" onclick="setCgPort('80,443')">Web (80, 443)</div>
1382 <div class="gen-pill" onclick="setCgPort('80,443,445,4430,50051')">RPC &amp; Enterprise</div>
1383 <div class="gen-pill" onclick="setCgPort('full')">Full Spectrum</div>
1384 </div>
1385 </div>
1386 </div>
1387
1388 <!-- Volume, Rate Limit & Pacing -->
1389 <div class="gen-grid">
1390 <div class="gen-field">
1391 <div class="gen-label">Data Volume &amp; Throughput Rate</div>
1392 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1393 <select class="gen-select" id="cgSize" onchange="updateGeneratedCommand()">
1394 <option value="100KB">100 KB</option>
1395 <option value="1MB" selected>1 MB</option>
1396 <option value="2MB">2 MB</option>
1397 <option value="5MB">5 MB</option>
1398 <option value="10MB">10 MB</option>
1399 <option value="50MB">50 MB</option>
1400 <option value="1GB">1 GB</option>
1401 </select>
1402 <select class="gen-select" id="cgThroughput" onchange="updateGeneratedCommand()">
1403 <option value="unlimited" selected>Unlimited Throughput</option>
1404 <option value="100KB/s">100 KB/s</option>
1405 <option value="500KB/s">500 KB/s</option>
1406 <option value="1MB/s">1 MB/s (1000 KB/s)</option>
1407 <option value="2MB/s">2 MB/s</option>
1408 <option value="5MB/s">5 MB/s</option>
1409 </select>
1410 </div>
1411 </div>
1412 <div class="gen-field">
1413 <div class="gen-label">Pacing Cadence &amp; Concurrency</div>
1414 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1415 <select class="gen-select" id="cgCadence" onchange="updateGeneratedCommand()">
1416 <option value="steady" selected>Steady (Default)</option>
1417 <option value="burst">Burst (Intermittent)</option>
1418 <option value="jittered">Jittered (Random Interval)</option>
1419 <option value="low-and-slow">Low &amp; Slow Stealth</option>
1420 <option value="office-hours">Office Hours Emulation</option>
1421 </select>
1422 <select class="gen-select" id="cgConcurrency" onchange="updateGeneratedCommand()">
1423 <option value="10">Concurrency: 10</option>
1424 <option value="50">Concurrency: 50</option>
1425 <option value="100" selected>Concurrency: 100</option>
1426 <option value="200">Concurrency: 200</option>
1427 </select>
1428 </div>
1429 </div>
1430 </div>
1431
1432 <!-- DLP & Evasion Options -->
1433 <div class="gen-grid">
1434 <div class="gen-field">
1435 <div class="gen-label">Synthetic DLP &amp; Carrier Format</div>
1436 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1437 <select class="gen-select" id="cgDLP" onchange="updateGeneratedCommand()">
1438 <option value="all" selected>All DLP Secrets</option>
1439 <option value="pci">PCI Cardholder Data</option>
1440 <option value="hipaa">HIPAA ePHI Records</option>
1441 <option value="pii">PII Identities</option>
1442 <option value="secrets">Cloud Secrets &amp; Keys</option>
1443 </select>
1444 <select class="gen-select" id="cgCarrier" onchange="updateGeneratedCommand()">
1445 <option value="csv" selected>CSV Spreadsheet</option>
1446 <option value="json">JSON API Stream</option>
1447 <option value="sql">SQL Dump Carrier</option>
1448 <option value="email">RFC 822 Email Body</option>
1449 <option value="syslog">Syslog Message</option>
1450 </select>
1451 </div>
1452 </div>
1453 <div class="gen-field">
1454 <div class="gen-label">Steganography &amp; Evasion Chain</div>
1455 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1456 <select class="gen-select" id="cgStego" onchange="updateGeneratedCommand()">
1457 <option value="none" selected>Stego: None</option>
1458 <option value="png">PNG Pixel LSB</option>
1459 <option value="bmp">BMP Raw Carrier</option>
1460 <option value="wav">WAV Audio Frequency</option>
1461 <option value="jpeg-zip">Polyglot JPEG-ZIP</option>
1462 <option value="pdf-jar">Polyglot PDF-JAR</option>
1463 </select>
1464 <select class="gen-select" id="cgTransforms" onchange="updateGeneratedCommand()">
1465 <option value="raw" selected>Raw (Unmodified)</option>
1466 <option value="base64">Base64 Encoded</option>
1467 <option value="hex">Hex Encoded</option>
1468 <option value="gzip">Gzip Compressed</option>
1469 <option value="zstd">Zstandard Compressed</option>
1470 <option value="aes_gcm">AES-256-GCM Encrypted</option>
1471 </select>
1472 </div>
1473 </div>
1474 </div>
1475
1476 <!-- Traversal & Reporting Checkboxes -->
1477 <div class="gen-checkbox-row">
1478 <label class="gen-checkbox-label">
1479 <input type="checkbox" id="cgSyncReport" checked onchange="updateGeneratedCommand()" />
1480 <span>Sync Telemetry &amp; Report to Live Dashboard (<code style="color: var(--cyan);">--sync-report</code>)</span>
1481 </label>
1482 <label class="gen-checkbox-label">
1483 <input type="checkbox" id="cgOutputHTML" checked onchange="updateGeneratedCommand()" />
1484 <span>Generate Standalone HTML Report</span>
1485 </label>
1486 <label class="gen-checkbox-label">
1487 <input type="checkbox" id="cgOutputJSON" onchange="updateGeneratedCommand()" />
1488 <span>Export Machine-Readable JSON</span>
1489 </label>
1490 <label class="gen-checkbox-label">
1491 <input type="checkbox" id="cgSavePayload" onchange="updateGeneratedCommand()" />
1492 <span>Retain Payload on Client Disk</span>
1493 </label>
1494 </div>
1495
1496 <!-- Live Generated Command Box -->
1497 <div class="gen-cmd-box">
1498 <div class="gen-cmd-head">
1499 <span>GENERATED CLIENT COMMAND LINE</span>
1500 <button class="nav-btn btn-cyan" id="cgCopyBtn" onclick="copyGeneratedCommand()">๐Ÿ“‹ COPY COMMAND</button>
1501 </div>
1502 <div class="gen-cmd-text" id="cgCmdText"></div>
1503 </div>
1504
1505 <!-- Companion Real-Time Inspection Helper -->
1506 <div style="background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px;">
1507 <div style="font-size: 11px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; display: flex; justify-content: space-between;">
1508 <span>Real-Time Packet Inspection Companion (Wireshark / Tshark)</span>
1509 <span style="cursor: pointer; color: var(--cyan);" onclick="copyTsharkCommand()">Copy Tshark</span>
1510 </div>
1511 <div style="font-family: var(--mono-font); font-size: 11px; color: var(--text-dim);" id="cgTsharkText">
1512 tshark -i any -f "udp port 443 or tcp port 80 or tcp port 443" -Y "quic or websocket or http"
1513 </div>
1514 </div>
1515
1516 </div>
1517
1518 <div class="modal-footer">
1519 <button class="nav-btn" onclick="closeCmdGenModal()">CLOSE</button>
1520 <button class="nav-btn btn-cyan" onclick="copyGeneratedCommand()">COPY CLI COMMAND</button>
1521 </div>
1522 </div>
1523 </div>
1524
1525 <script>
1526 // Max values for radial dial scales
1527 const MAX_THROUGHPUT = 100.0; // 100 MB/s
1528 const MAX_CONCURRENCY = 32.0;
1529
1530 const CIRCUMFERENCE = 408.4; // 2 * PI * 65
1531
1532 let lastActiveSessions = [];
1533 let lastRecentSessions = [];
1534
1535 function setDialProgress(elementId, value, maxVal) {
1536 const el = document.getElementById(elementId);
1537 if (!el) return;
1538 const pct = Math.min(1.0, Math.max(0.0, value / maxVal));
1539 const offset = CIRCUMFERENCE - (pct * CIRCUMFERENCE);
1540 el.style.strokeDasharray = CIRCUMFERENCE;
1541 el.style.strokeDashoffset = offset;
1542 }
1543
1544 function updateEEIColor(score) {
1545 const card = document.getElementById('cardEEI');
1546 const dialProg = document.getElementById('dialEEIProg');
1547 let strokeColor = 'var(--green)';
1548 let shadowColor = 'var(--green-glow)';
1549
1550 if (score > 80) {
1551 strokeColor = 'var(--crimson)';
1552 shadowColor = 'var(--crimson-glow)';
1553 } else if (score > 50) {
1554 strokeColor = 'var(--amber)';
1555 shadowColor = 'var(--amber-glow)';
1556 } else if (score > 20) {
1557 strokeColor = 'var(--blue)';
1558 shadowColor = 'var(--blue-glow)';
1559 }
1560
1561 if (dialProg) {
1562 dialProg.style.stroke = strokeColor;
1563 dialProg.style.filter = `drop-shadow(0 0 8px ${shadowColor})`;
1564 }
1565 }
1566
1567 function renderActiveStreams(activeStreams, recentStreams) {
1568 const feed = document.getElementById('activeStreamsFeed');
1569 const badge = document.getElementById('activeStreamsBadge');
1570 if (!feed || !badge) return;
1571
1572 const activeList = activeStreams || [];
1573 const recentList = recentStreams || [];
1574
1575 if (activeList.length > 0) {
1576 badge.textContent = `${activeList.length} Active // ${recentList.length} Logged`;
1577 } else {
1578 badge.textContent = recentList.length > 0 ? `${recentList.length} Logged (Standby)` : '0 Active';
1579 }
1580
1581 if (activeList.length === 0 && recentList.length === 0) {
1582 feed.innerHTML = '<div class="empty-state">No active or recorded exfiltration streams. Standby for client triggers...</div>';
1583 return;
1584 }
1585
1586 let html = '';
1587
1588 // 1. Render Active in-flight streams at the top
1589 activeList.forEach(s => {
1590 const zone = s.network_zone || 'DEFAULT';
1591 const host = s.client_host || s.client_ip || 'Remote Host';
1592 const proto = s.protocol || 'STREAM';
1593 const rate = (s.instant_mbs || 0).toFixed(2);
1594 const pct = (s.progress_pct || 0).toFixed(1);
1595 const transferred = formatBytes(s.bytes_transferred || 0);
1596 const total = formatBytes(s.total_bytes || 0);
1597 const seq = s.sequence || 0;
1598 const totalChunks = s.total_chunks || 0;
1599
1600 html += `
1601 <div class="stream-row stream-row-active" id="stream-${escapeHtml(s.session_id)}">
1602 <div class="host-info" title="${escapeHtml(host)}">${escapeHtml(host)}</div>
1603 <div><span class="zone-pill" title="${escapeHtml(zone)}">${escapeHtml(zone)}</span></div>
1604 <div><span class="proto-pill">${escapeHtml(proto)}</span></div>
1605 <div class="progress-bar-container">
1606 <div class="progress-track">
1607 <div class="progress-fill" style="width: ${pct}%;"></div>
1608 </div>
1609 <div class="progress-labels">
1610 <span>Chunk ${seq}/${totalChunks} (${transferred} / ${total})</span>
1611 <span>${pct}%</span>
1612 </div>
1613 </div>
1614 <div class="rate-val">${rate} MB/s</div>
1615 <div><span class="status-badge status-streaming">STREAMING</span></div>
1616 </div>
1617 `;
1618 });
1619
1620 // 2. Render Recent completed/blocked sessions directly below (up to 12 recent sessions)
1621 const displayedRecent = recentList.slice(0, 12);
1622 displayedRecent.forEach(s => {
1623 const zone = s.network_zone || 'DEFAULT';
1624 const host = s.client_host || s.client_ip || 'Remote Host';
1625 const proto = s.protocol || 'STREAM';
1626 const rate = (s.average_mbs || 0).toFixed(2);
1627 const total = formatBytes(s.total_bytes || 0);
1628 const durationSec = (s.duration_ms ? s.duration_ms / 1000 : 0).toFixed(1);
1629 const shortId = (s.session_id && s.session_id.length > 8) ? s.session_id.substring(0, 8) : (s.session_id || 'sess');
1630 const verdict = s.verdict || 'EXFIL_SUCCESS';
1631 const isBlocked = verdict.indexOf('BLOCKED') !== -1;
1632 const statusClass = isBlocked ? 'status-blocked' : 'status-success';
1633 const fillClass = isBlocked ? 'fill-blocked' : 'fill-success';
1634
1635 html += `
1636 <div class="stream-row stream-row-recent">
1637 <div class="host-info" title="${escapeHtml(host)}">${escapeHtml(host)}</div>
1638 <div><span class="zone-pill" title="${escapeHtml(zone)}">${escapeHtml(zone)}</span></div>
1639 <div><span class="proto-pill">${escapeHtml(proto)}</span></div>
1640 <div class="progress-bar-container">
1641 <div class="progress-track">
1642 <div class="progress-fill ${fillClass}" style="width: 100%;"></div>
1643 </div>
1644 <div class="progress-labels">
1645 <span>Session ${shortId} (${total} in ${durationSec}s ยท ${s.total_chunks || 0} chunks)</span>
1646 <span>100%</span>
1647 </div>
1648 </div>
1649 <div class="rate-val" style="color: ${isBlocked ? 'var(--crimson)' : 'var(--green)'};">${rate} MB/s</div>
1650 <div><span class="status-badge ${statusClass}">${escapeHtml(verdict)}</span></div>
1651 </div>
1652 `;
1653 });
1654
1655 feed.innerHTML = html;
1656 }
1657
1658 function updateStreamRowInPlace(row, s) {
1659 if (!row || !s) return;
1660 const rate = (s.instant_mbs || 0).toFixed(2);
1661 const pct = (s.progress_pct || 0).toFixed(1);
1662 const transferred = formatBytes(s.bytes_transferred || 0);
1663 const total = formatBytes(s.total_bytes || 0);
1664 const seq = s.sequence || 0;
1665 const totalChunks = s.total_chunks || 0;
1666
1667 const fill = row.querySelector('.progress-fill');
1668 if (fill) fill.style.width = `${pct}%`;
1669
1670 const labels = row.querySelector('.progress-labels');
1671 if (labels) {
1672 labels.innerHTML = `<span>Chunk ${seq}/${totalChunks} (${transferred} / ${total})</span><span>${pct}%</span>`;
1673 }
1674
1675 const rateEl = row.querySelector('.rate-val');
1676 if (rateEl) rateEl.textContent = `${rate} MB/s`;
1677 }
1678
1679 function renderProtocols(protocols) {
1680 const grid = document.getElementById('protocolGrid');
1681 if (!grid) return;
1682
1683 let html = '';
1684 Object.keys(protocols).sort().forEach(p => {
1685 const ps = protocols[p];
1686 const status = ps.status || 'ready';
1687 let beaconClass = 'beacon-ready';
1688 let cardClass = '';
1689
1690 if (status === 'streaming') {
1691 beaconClass = 'beacon-streaming';
1692 cardClass = 'active-stream';
1693 } else if (status === 'blocked') {
1694 beaconClass = 'beacon-blocked';
1695 cardClass = 'blocked-state';
1696 }
1697
1698 html += `
1699 <div class="proto-card ${cardClass}" id="proto-card-${p}">
1700 <div class="proto-header">
1701 <span class="proto-name">${p}</span>
1702 <div class="proto-beacon ${beaconClass}" id="proto-beacon-${p}"></div>
1703 </div>
1704 <div class="proto-vol" id="proto-vol-${p}">${ps.bytes_formatted || '0 B'}</div>
1705 </div>
1706 `;
1707 });
1708 grid.innerHTML = html;
1709 }
1710
1711 function renderPorts(ports) {
1712 const grid = document.getElementById('portsGrid');
1713 if (!grid || !ports) return;
1714
1715 let html = '';
1716 ports.forEach(p => {
1717 const status = p.status || 'listening';
1718 let beaconClass = 'beacon-listening';
1719 let cardClass = 'status-listening';
1720
1721 if (status === 'receiving') {
1722 beaconClass = 'beacon-receiving';
1723 cardClass = 'status-receiving';
1724 } else if (status === 'reflection') {
1725 beaconClass = 'beacon-reflection';
1726 cardClass = 'status-reflection';
1727 } else if (status === 'blocked') {
1728 beaconClass = 'beacon-port-blocked';
1729 cardClass = 'status-blocked';
1730 } else if (status === 'idle') {
1731 beaconClass = 'beacon-idle';
1732 cardClass = '';
1733 }
1734
1735 html += `
1736 <div class="port-card ${cardClass}" id="port-card-${p.port}">
1737 <div class="port-header">
1738 <span class="port-num">:${p.port}</span>
1739 <div class="port-beacon ${beaconClass}" id="port-beacon-${p.port}" title="${status.toUpperCase()}"></div>
1740 </div>
1741 <div class="port-meta">
1742 <span>${p.service || p.protocol}</span>
1743 <span id="port-status-${p.port}" style="color: ${status === 'reflection' ? 'var(--amber)' : (status === 'receiving' ? 'var(--cyan)' : (status === 'blocked' ? 'var(--crimson)' : 'var(--emerald)'))}; font-weight:700;">${status.toUpperCase()}</span>
1744 </div>
1745 <div class="port-meta" style="font-size: 9px; opacity: 0.8;">
1746 <span>${p.protocol}</span>
1747 <span id="port-bytes-${p.port}">${formatBytes(p.bytes_recv || 0)}</span>
1748 </div>
1749 </div>
1750 `;
1751 });
1752 grid.innerHTML = html;
1753 }
1754
1755 let allLogs = [];
1756 let currentLogFilter = 'ALL';
1757 let isAutoScroll = true;
1758 let isAutoScrollUserPref = true;
1759 let isLogNowrap = false;
1760 let isLogExpanded = false;
1761
1762 function createLogElement(entry) {
1763 const line = document.createElement('div');
1764 line.className = 'log-line';
1765
1766 const time = entry.timestamp || '00:00:00.000';
1767 const lvl = entry.level || 'INFO';
1768 const zone = entry.zone ? `[${entry.zone}]` : '';
1769 let msg = entry.message || '';
1770 let proto = entry.protocol ? entry.protocol.toUpperCase() : '';
1771
1772 // If message begins with [protocol], strip it so we don't duplicate badge
1773 if (entry.protocol && msg.toLowerCase().startsWith('[' + entry.protocol.toLowerCase() + ']')) {
1774 msg = msg.substring(entry.protocol.length + 2).trim();
1775 }
1776
1777 line.innerHTML = `
1778 <span class="log-time">[${time}]</span>
1779 <span class="log-lvl lvl-${lvl}">[${lvl}]</span>
1780 ${zone ? `<span class="log-zone">${escapeHtml(zone)}</span>` : ''}
1781 ${proto ? `<span class="log-proto">[${escapeHtml(proto)}]</span>` : ''}
1782 <span class="log-msg">${escapeHtml(msg)}</span>
1783 `;
1784 return line;
1785 }
1786
1787 const seenLogKeys = new Set();
1788
1789 function getLogKey(entry) {
1790 if (!entry) return '';
1791 if (entry.id) return entry.id;
1792 return `${entry.level || ''}_${entry.protocol || ''}_${entry.message || ''}`;
1793 }
1794
1795 function addLogToTerminal(entry) {
1796 if (!entry) return;
1797 const key = getLogKey(entry);
1798 if (key && seenLogKeys.has(key)) return;
1799 if (key) seenLogKeys.add(key);
1800
1801 allLogs.push(entry);
1802 if (allLogs.length > 500) {
1803 const removed = allLogs.shift();
1804 if (removed) {
1805 const remKey = getLogKey(removed);
1806 if (remKey) seenLogKeys.delete(remKey);
1807 }
1808 }
1809 updateLogBadge();
1810
1811 if (currentLogFilter !== 'ALL' && entry.level !== currentLogFilter) {
1812 return;
1813 }
1814
1815 const term = document.getElementById('terminalLogs');
1816 if (!term) return;
1817
1818 const el = createLogElement(entry);
1819 term.appendChild(el);
1820
1821 if (isAutoScroll) {
1822 term.scrollTop = term.scrollHeight;
1823 }
1824 }
1825
1826 function renderLogs() {
1827 const term = document.getElementById('terminalLogs');
1828 if (!term) return;
1829 term.innerHTML = '';
1830
1831 const filtered = currentLogFilter === 'ALL'
1832 ? allLogs
1833 : allLogs.filter(e => e.level === currentLogFilter);
1834
1835 filtered.forEach(entry => {
1836 term.appendChild(createLogElement(entry));
1837 });
1838
1839 if (isAutoScroll) {
1840 term.scrollTop = term.scrollHeight;
1841 }
1842 }
1843
1844 function updateLogBadge() {
1845 const badge = document.getElementById('logCountBadge');
1846 if (badge) {
1847 badge.textContent = `${allLogs.length} Events`;
1848 }
1849 }
1850
1851 function setLogFilter(filter) {
1852 currentLogFilter = filter;
1853 document.querySelectorAll('#logFilterGroup .filter-pill').forEach(btn => {
1854 btn.classList.toggle('active', btn.id === `filterBtn${filter}`);
1855 });
1856 renderLogs();
1857 }
1858
1859 function toggleAutoScroll(enabled) {
1860 isAutoScroll = enabled;
1861 isAutoScrollUserPref = enabled;
1862 if (enabled) {
1863 const term = document.getElementById('terminalLogs');
1864 if (term) term.scrollTop = term.scrollHeight;
1865 }
1866 }
1867
1868 let isLogWide = false;
1869 let isLogFullscreen = false;
1870
1871 function toggleLogWrap() {
1872 isLogNowrap = !isLogNowrap;
1873 const term = document.getElementById('terminalLogs');
1874 const btn = document.getElementById('wrapToggleBtn');
1875 if (term) term.classList.toggle('nowrap', isLogNowrap);
1876 if (btn) btn.textContent = isLogNowrap ? 'WRAP' : 'NOWRAP';
1877 }
1878
1879 function toggleLogWide() {
1880 isLogWide = !isLogWide;
1881 const section = document.getElementById('liveEventStreamSection');
1882 const btn = document.getElementById('wideToggleBtn');
1883 if (section) section.classList.toggle('full-bleed', isLogWide);
1884 if (btn) btn.textContent = isLogWide ? 'โ†” DEFAULT' : 'โ†” WIDE';
1885 }
1886
1887 function toggleLogExpand() {
1888 isLogExpanded = !isLogExpanded;
1889 const card = document.getElementById('terminalCard');
1890 const btn = document.getElementById('expandToggleBtn');
1891 if (card) card.classList.toggle('expanded', isLogExpanded);
1892 if (btn) btn.textContent = isLogExpanded ? 'โ†• COMPACT' : 'โ†• TALL';
1893 }
1894
1895 function toggleLogFullscreen() {
1896 isLogFullscreen = !isLogFullscreen;
1897 const card = document.getElementById('terminalCard');
1898 const btn = document.getElementById('fullscreenToggleBtn');
1899 document.body.classList.toggle('terminal-fullscreen-active', isLogFullscreen);
1900 if (card) card.classList.toggle('fullscreen', isLogFullscreen);
1901 if (btn) btn.textContent = isLogFullscreen ? 'โœ• RESTORE' : 'โ›ถ MAXIMIZE';
1902 }
1903
1904 document.addEventListener('keydown', (e) => {
1905 if (e.key === 'Escape' && isLogFullscreen) {
1906 toggleLogFullscreen();
1907 }
1908 });
1909
1910 function clearLogs() {
1911 allLogs = [];
1912 seenLogKeys.clear();
1913 const term = document.getElementById('terminalLogs');
1914 if (term) term.innerHTML = '';
1915 updateLogBadge();
1916 }
1917
1918 function initTerminalScrollListener() {
1919 const term = document.getElementById('terminalLogs');
1920 if (!term) return;
1921 term.addEventListener('scroll', () => {
1922 const isNearBottom = term.scrollHeight - term.scrollTop - term.clientHeight < 40;
1923 const toggle = document.getElementById('autoScrollToggle');
1924 if (!isNearBottom && isAutoScroll) {
1925 isAutoScroll = false;
1926 if (toggle) toggle.checked = false;
1927 } else if (isNearBottom && !isAutoScroll && isAutoScrollUserPref) {
1928 isAutoScroll = true;
1929 if (toggle) toggle.checked = true;
1930 }
1931 });
1932 }
1933
1934 function renderReports(reports) {
1935 const tbody = document.getElementById('reportsTableBody');
1936 const badge = document.getElementById('reportsCountBadge');
1937 if (!tbody || !badge) return;
1938
1939 badge.textContent = `${reports.length} Stored`;
1940
1941 if (reports.length === 0) {
1942 tbody.innerHTML = `
1943 <tr>
1944 <td colspan="8" style="text-align: center; color: var(--text-dim); padding: 24px;">No client reports synchronized yet. Run client with <code>--sync-report &lt;dashboard-url&gt;</code> to upload findings.</td>
1945 </tr>
1946 `;
1947 return;
1948 }
1949
1950 let html = '';
1951 reports.forEach(r => {
1952 const id = r.report_id;
1953 const shortId = id.length > 8 ? id.substring(0, 8) : id;
1954 const eei = (r.eei_score || 0).toFixed(2);
1955 const host = r.client_host || r.client_ip || 'Remote Host';
1956 const zone = r.network_zone || 'DEFAULT';
1957
1958 html += `
1959 <tr>
1960 <td style="font-family: var(--mono-font); font-weight: 700; color: var(--cyan);">${shortId}</td>
1961 <td style="font-family: var(--mono-font);">${escapeHtml(r.target_host)}</td>
1962 <td><span class="zone-pill">${escapeHtml(zone)}</span></td>
1963 <td style="font-family: var(--mono-font);">${escapeHtml(host)}</td>
1964 <td style="font-family: var(--mono-font); font-weight: 700; color: ${r.eei_score > 50 ? 'var(--crimson)' : 'var(--green)'};">${eei}% (${r.posture || 'Normal'})</td>
1965 <td style="font-family: var(--mono-font);">${r.total_probes} (${r.success_count} pass / ${r.blocked_l4 + r.blocked_l7 + r.blocked_dlp} blocked)</td>
1966 <td style="font-family: var(--mono-font); color: var(--text-dim);">${r.uploaded_at ? r.uploaded_at.substring(11, 19) : ''}</td>
1967 <td>
1968 <div class="report-actions">
1969 <a href="#" class="action-link" onclick="openReportModal('${id}')">VIEW HTML</a>
1970 <a href="/api/v1/reports/${id}/json" target="_blank" class="action-link">JSON</a>
1971 <a href="/api/v1/reports/${id}/sarif" target="_blank" class="action-link">SARIF</a>
1972 <a href="/api/v1/reports/${id}/remediation" target="_blank" class="action-link">RULES</a>
1973 </div>
1974 </td>
1975 </tr>
1976 `;
1977 });
1978 tbody.innerHTML = html;
1979 }
1980
1981 function openReportModal(reportId) {
1982 const modal = document.getElementById('reportModal');
1983 const iframe = document.getElementById('reportIframe');
1984 const title = document.getElementById('modalReportTitle');
1985 if (!modal || !iframe) return;
1986
1987 title.textContent = `ASSESSMENT REPORT: ${reportId}`;
1988 iframe.src = `/api/v1/reports/${reportId}/html`;
1989 modal.style.display = 'flex';
1990 }
1991
1992 function closeReportModal() {
1993 const modal = document.getElementById('reportModal');
1994 const iframe = document.getElementById('reportIframe');
1995 if (modal) modal.style.display = 'none';
1996 if (iframe) iframe.src = '';
1997 }
1998
1999 function scrollToReports() {
2000 const el = document.getElementById('reportsSection');
2001 if (el) el.scrollIntoView({ behavior: 'smooth' });
2002 }
2003
2004 function openResetModal() {
2005 const modal = document.getElementById('resetModal');
2006 const fb = document.getElementById('resetFeedback');
2007 const chk = document.getElementById('resetDeleteFiles');
2008 const btn = document.getElementById('confirmResetBtn');
2009 if (fb) fb.innerHTML = '';
2010 if (chk) chk.checked = false;
2011 if (btn) {
2012 btn.disabled = false;
2013 btn.textContent = 'CONFIRM RESET';
2014 }
2015 if (modal) modal.style.display = 'flex';
2016 }
2017
2018 function closeResetModal(e) {
2019 if (e && e.target && e.target.id !== 'resetModal') return;
2020 const modal = document.getElementById('resetModal');
2021 if (modal) modal.style.display = 'none';
2022 }
2023
2024 // Client CLI Command Generator Modal functions
2025 function openCmdGenModal() {
2026 const modal = document.getElementById('cmdGenModal');
2027 if (!modal) return;
2028 modal.style.display = 'flex';
2029 const srv = document.getElementById('cgServer');
2030 if (srv && (srv.value === '127.0.0.1' || !srv.value)) {
2031 const h = window.location.hostname;
2032 if (h && h !== 'localhost' && h !== '127.0.0.1') {
2033 srv.value = h;
2034 }
2035 }
2036 syncProtoPillsFromInput();
2037 updateGeneratedCommand();
2038 }
2039
2040 function closeCmdGenModal(e) {
2041 if (e && e.target && e.target.id !== 'cmdGenModal') return;
2042 const modal = document.getElementById('cmdGenModal');
2043 if (modal) modal.style.display = 'none';
2044 }
2045
2046 function setCgPort(val) {
2047 const el = document.getElementById('cgPorts');
2048 if (el) {
2049 el.value = val;
2050 updateGeneratedCommand();
2051 }
2052 }
2053
2054 function selectAllProtocols() {
2055 const el = document.getElementById('cgProtocols');
2056 if (el) {
2057 el.value = 'all';
2058 syncProtoPillsFromInput();
2059 updateGeneratedCommand();
2060 }
2061 }
2062
2063 function toggleProtoPill(proto) {
2064 const el = document.getElementById('cgProtocols');
2065 if (!el) return;
2066 let curr = el.value.trim().toLowerCase();
2067 if (curr === 'all') {
2068 el.value = proto;
2069 } else {
2070 let parts = curr.split(',').map(s => s.trim()).filter(Boolean);
2071 const idx = parts.indexOf(proto);
2072 if (idx >= 0) {
2073 parts.splice(idx, 1);
2074 } else {
2075 parts.push(proto);
2076 }
2077 el.value = parts.length > 0 ? parts.join(',') : 'all';
2078 }
2079 syncProtoPillsFromInput();
2080 updateGeneratedCommand();
2081 }
2082
2083 function syncProtoPillsFromInput() {
2084 const el = document.getElementById('cgProtocols');
2085 if (!el) return;
2086 const curr = el.value.trim().toLowerCase();
2087 const parts = curr === 'all' ? ['all'] : curr.split(',').map(s => s.trim());
2088 document.querySelectorAll('#cgProtoPills .gen-pill').forEach(pill => {
2089 const p = pill.getAttribute('data-proto');
2090 if (curr === 'all' || parts.includes(p)) {
2091 pill.classList.add('active');
2092 } else {
2093 pill.classList.remove('active');
2094 }
2095 });
2096 }
2097
2098 function applyCgPreset(preset) {
2099 document.querySelectorAll('#cgPresets .gen-pill').forEach(p => p.classList.remove('active'));
2100 if (window.event && window.event.target) {
2101 window.event.target.classList.add('active');
2102 }
2103
2104 const protoEl = document.getElementById('cgProtocols');
2105 const portsEl = document.getElementById('cgPorts');
2106 const sizeEl = document.getElementById('cgSize');
2107 const rateEl = document.getElementById('cgThroughput');
2108 const cadenceEl = document.getElementById('cgCadence');
2109 const dlpEl = document.getElementById('cgDLP');
2110 const carrierEl = document.getElementById('cgCarrier');
2111 const stegoEl = document.getElementById('cgStego');
2112 const transformsEl = document.getElementById('cgTransforms');
2113
2114 if (preset === 'full') {
2115 if (protoEl) protoEl.value = 'all';
2116 if (portsEl) portsEl.value = 'quick';
2117 if (sizeEl) sizeEl.value = '1MB';
2118 if (rateEl) rateEl.value = 'unlimited';
2119 if (cadenceEl) cadenceEl.value = 'steady';
2120 if (dlpEl) dlpEl.value = 'all';
2121 if (carrierEl) carrierEl.value = 'csv';
2122 if (stegoEl) stegoEl.value = 'none';
2123 if (transformsEl) transformsEl.value = 'raw';
2124 } else if (preset === 'quic-ws') {
2125 if (protoEl) protoEl.value = 'http3,websocket';
2126 if (portsEl) portsEl.value = '443,80';
2127 if (sizeEl) sizeEl.value = '2MB';
2128 if (rateEl) rateEl.value = '1MB/s';
2129 if (cadenceEl) cadenceEl.value = 'steady';
2130 if (dlpEl) dlpEl.value = 'pci';
2131 if (carrierEl) carrierEl.value = 'csv';
2132 } else if (preset === 'stealth') {
2133 if (protoEl) protoEl.value = 'dns_a,dns_txt,doh,icmp,ntp';
2134 if (portsEl) portsEl.value = '53,853,443';
2135 if (sizeEl) sizeEl.value = '100KB';
2136 if (rateEl) rateEl.value = '100KB/s';
2137 if (cadenceEl) cadenceEl.value = 'jittered';
2138 if (dlpEl) dlpEl.value = 'secrets';
2139 if (transformsEl) transformsEl.value = 'base64';
2140 } else if (preset === 'proxy') {
2141 if (protoEl) protoEl.value = 'http1,webdav';
2142 if (portsEl) portsEl.value = '80,8080';
2143 if (sizeEl) sizeEl.value = '500KB';
2144 if (cadenceEl) cadenceEl.value = 'steady';
2145 } else if (preset === 'smb') {
2146 if (protoEl) protoEl.value = 'smb_file,smb_hash,smb_quic';
2147 if (portsEl) portsEl.value = '445,4430';
2148 if (sizeEl) sizeEl.value = '1MB';
2149 } else if (preset === 'dlp') {
2150 if (protoEl) protoEl.value = 'http2,http3,websocket';
2151 if (portsEl) portsEl.value = '443';
2152 if (sizeEl) sizeEl.value = '5MB';
2153 if (dlpEl) dlpEl.value = 'pci';
2154 if (carrierEl) carrierEl.value = 'json';
2155 }
2156
2157 syncProtoPillsFromInput();
2158 updateGeneratedCommand();
2159 }
2160
2161 function updateGeneratedCommand() {
2162 const platform = (document.getElementById('cgPlatform') || {}).value || './bin/contraband client';
2163 const server = ((document.getElementById('cgServer') || {}).value || '127.0.0.1').trim();
2164 const protos = ((document.getElementById('cgProtocols') || {}).value || 'all').trim();
2165 const ports = ((document.getElementById('cgPorts') || {}).value || 'quick').trim();
2166 const size = (document.getElementById('cgSize') || {}).value || '1MB';
2167 const throughput = (document.getElementById('cgThroughput') || {}).value || 'unlimited';
2168 const cadence = (document.getElementById('cgCadence') || {}).value || 'steady';
2169 const concurrency = (document.getElementById('cgConcurrency') || {}).value || '100';
2170 const dlp = (document.getElementById('cgDLP') || {}).value || 'all';
2171 const carrier = (document.getElementById('cgCarrier') || {}).value || 'csv';
2172 const stego = (document.getElementById('cgStego') || {}).value || 'none';
2173 const transforms = (document.getElementById('cgTransforms') || {}).value || 'raw';
2174
2175 const syncReport = (document.getElementById('cgSyncReport') || {}).checked;
2176 const outHTML = (document.getElementById('cgOutputHTML') || {}).checked;
2177 const outJSON = (document.getElementById('cgOutputJSON') || {}).checked;
2178 const savePayload = (document.getElementById('cgSavePayload') || {}).checked;
2179
2180 let cmd = `${platform} --server ${server}`;
2181
2182 if (ports && ports !== 'quick') {
2183 cmd += ` --ports ${ports}`;
2184 } else {
2185 cmd += ` --ports quick`;
2186 }
2187
2188 if (protos && protos !== 'all') {
2189 cmd += ` --protocols ${protos}`;
2190 }
2191
2192 if (size && size !== '1MB') {
2193 cmd += ` --size ${size}`;
2194 }
2195
2196 if (throughput && throughput !== 'unlimited') {
2197 cmd += ` --throughput ${throughput}`;
2198 }
2199
2200 if (cadence && cadence !== 'steady') {
2201 cmd += ` --cadence ${cadence}`;
2202 }
2203
2204 if (concurrency && concurrency !== '100') {
2205 cmd += ` --concurrency ${concurrency}`;
2206 }
2207
2208 if (dlp && dlp !== 'all') {
2209 cmd += ` --dlp ${dlp}`;
2210 }
2211
2212 if (carrier && carrier !== 'csv') {
2213 cmd += ` --carrier-type ${carrier}`;
2214 }
2215
2216 if (stego && stego !== 'none') {
2217 cmd += ` --stego ${stego}`;
2218 }
2219
2220 if (transforms && transforms !== 'raw') {
2221 cmd += ` --transforms ${transforms}`;
2222 }
2223
2224 if (syncReport) {
2225 const port = window.location.port ? window.location.port : '8080';
2226 cmd += ` --sync-report http://${server}:${port}`;
2227 }
2228
2229 if (outHTML) {
2230 cmd += ` --output-html evidence/assessment.html`;
2231 }
2232
2233 if (outJSON) {
2234 cmd += ` --output-json evidence/assessment.json`;
2235 }
2236
2237 if (savePayload) {
2238 cmd += ` --save-payload`;
2239 }
2240
2241 const outEl = document.getElementById('cgCmdText');
2242 if (outEl) outEl.textContent = cmd;
2243
2244 // Update companion tshark helper
2245 const tsharkEl = document.getElementById('cgTsharkText');
2246 if (tsharkEl) {
2247 let portFilter = "port 80 or port 443";
2248 if (protos.includes('dns')) portFilter += " or port 53 or port 853";
2249 if (protos.includes('smb')) portFilter += " or port 445 or port 4430";
2250 if (protos.includes('grpc')) portFilter += " or port 50051";
2251 tsharkEl.textContent = `tshark -i any -f "${portFilter}" -Y "quic or websocket or http or dns or smb2"`;
2252 }
2253 }
2254
2255 function copyGeneratedCommand() {
2256 const text = (document.getElementById('cgCmdText') || {}).textContent || '';
2257 if (!text) return;
2258 navigator.clipboard.writeText(text).then(() => {
2259 const btn = document.getElementById('cgCopyBtn');
2260 if (btn) {
2261 const orig = btn.textContent;
2262 btn.textContent = 'COPIED! โœ“';
2263 btn.style.color = 'var(--green)';
2264 btn.style.borderColor = 'var(--green)';
2265 setTimeout(() => {
2266 btn.textContent = orig;
2267 btn.style.color = '';
2268 btn.style.borderColor = '';
2269 }, 1600);
2270 }
2271 });
2272 }
2273
2274 function copyTsharkCommand() {
2275 const text = (document.getElementById('cgTsharkText') || {}).textContent || '';
2276 if (!text) return;
2277 navigator.clipboard.writeText(text).then(() => {
2278 const orig = document.querySelector('#cgTsharkText').style.color;
2279 document.querySelector('#cgTsharkText').style.color = 'var(--green)';
2280 setTimeout(() => {
2281 document.querySelector('#cgTsharkText').style.color = orig;
2282 }, 1200);
2283 });
2284 }
2285
2286 function executeReset() {
2287 const chk = document.getElementById('resetDeleteFiles');
2288 const deleteFiles = chk ? chk.checked : false;
2289 const fb = document.getElementById('resetFeedback');
2290 const btn = document.getElementById('confirmResetBtn');
2291
2292 if (btn) {
2293 btn.disabled = true;
2294 btn.textContent = 'RESETTING...';
2295 }
2296 if (fb) {
2297 fb.style.color = 'var(--cyan)';
2298 fb.textContent = 'Executing telemetry reset...';
2299 }
2300
2301 fetch('/api/v1/live/reset', {
2302 method: 'POST',
2303 headers: { 'Content-Type': 'application/json' },
2304 body: JSON.stringify({ delete_files: deleteFiles })
2305 })
2306 .then(res => {
2307 if (!res.ok) throw new Error('Reset request failed: HTTP ' + res.status);
2308 return res.json();
2309 })
2310 .then(data => {
2311 if (fb) {
2312 fb.style.color = 'var(--green)';
2313 fb.textContent = data.message || 'Dashboard state reset successfully.';
2314 }
2315 setTimeout(() => {
2316 closeResetModal();
2317 }, 800);
2318 })
2319 .catch(err => {
2320 if (fb) {
2321 fb.style.color = 'var(--crimson)';
2322 fb.textContent = 'Reset error: ' + err.message;
2323 }
2324 if (btn) {
2325 btn.disabled = false;
2326 btn.textContent = 'CONFIRM RESET';
2327 }
2328 });
2329 }
2330
2331 function handleResetEvent(data) {
2332 // 1. Reset radial dials
2333 document.getElementById('dialThroughputVal').textContent = '0.00';
2334 setDialProgress('dialThroughputProg', 0, MAX_THROUGHPUT);
2335 document.getElementById('dialPeakThroughput').textContent = 'Peak: 0.00 MB/s';
2336
2337 document.getElementById('dialVolumeVal').textContent = '0 B';
2338 setDialProgress('dialVolumeProg', 0, 1024 * 1024 * 1024);
2339 document.getElementById('dialTotalChunks').textContent = '0 Chunks Assembled';
2340
2341 document.getElementById('dialEEIVal').textContent = '0%';
2342 document.getElementById('dialEEIPosture').textContent = 'HARDENED';
2343 setDialProgress('dialEEIProg', 0, 100.0);
2344 updateEEIColor(0);
2345
2346 document.getElementById('dialConcVal').textContent = '0';
2347 setDialProgress('dialConcProg', 0, MAX_CONCURRENCY);
2348 document.getElementById('dialTotalSessions').textContent = '0 Total Sessions';
2349
2350 // 2. Clear Active & Recent Streams
2351 lastActiveSessions = [];
2352 lastRecentSessions = [];
2353 renderActiveStreams([], []);
2354
2355 // 3. Clear logs
2356 clearLogs();
2357
2358 // 4. Re-fetch metrics from server
2359 fetchMetrics();
2360 }
2361
2362 function formatBytes(bytes) {
2363 if (bytes < 1024) return bytes + ' B';
2364 const units = ['KB', 'MB', 'GB', 'TB'];
2365 let i = -1;
2366 do {
2367 bytes = bytes / 1024;
2368 i++;
2369 } while (bytes >= 1024 && i < units.length - 1);
2370 return bytes.toFixed(2) + ' ' + units[i];
2371 }
2372
2373 function escapeHtml(str) {
2374 if (!str) return '';
2375 return String(str)
2376 .replace(/&/g, '&amp;')
2377 .replace(/</g, '&lt;')
2378 .replace(/>/g, '&gt;')
2379 .replace(/"/g, '&quot;')
2380 .replace(/'/g, '&#039;');
2381 }
2382
2383 const PROTO_TO_PORT = {
2384 'http_post': 80,
2385 'http1': 80,
2386 'http': 80,
2387 'webdav': 80,
2388 'websocket_binary': 80,
2389 'websocket': 80,
2390 'ws': 80,
2391 'https_post': 443,
2392 'http2': 443,
2393 'https': 443,
2394 'http3_quic': 443,
2395 'http3': 443,
2396 'quic': 443,
2397 'aws_s3': 443,
2398 'gcp_gcs': 443,
2399 'azure_blob': 443,
2400 'dns_a': 53,
2401 'dns_txt': 53,
2402 'ftp': 21,
2403 'ftps': 990,
2404 'sftp': 22,
2405 'smtp': 25,
2406 'smb_file': 445,
2407 'smb_hash': 445,
2408 'smb_quic': 4430,
2409 'ntp_time': 123,
2410 'syslog': 514,
2411 'mqtt': 1883,
2412 'grpc_protobuf': 50051,
2413 'dns_over_tls': 853,
2414 'dns_over_https': 8443
2415 };
2416
2417 const portDecayTimers = {};
2418 function setPortReceivingInPlace(port) {
2419 const card = document.getElementById('port-card-' + port);
2420 const beacon = document.getElementById('port-beacon-' + port);
2421 const statusLabel = document.getElementById('port-status-' + port);
2422 if (card && beacon && statusLabel) {
2423 card.className = 'port-card status-receiving';
2424 beacon.className = 'port-beacon beacon-receiving';
2425 beacon.title = 'RECEIVING';
2426 statusLabel.style.color = 'var(--cyan)';
2427 statusLabel.textContent = 'RECEIVING';
2428
2429 if (portDecayTimers[port]) {
2430 clearTimeout(portDecayTimers[port]);
2431 }
2432 portDecayTimers[port] = setTimeout(() => {
2433 delete portDecayTimers[port];
2434 card.className = 'port-card status-listening';
2435 beacon.className = 'port-beacon beacon-listening';
2436 beacon.title = 'LISTENING';
2437 statusLabel.style.color = 'var(--emerald)';
2438 statusLabel.textContent = 'LISTENING';
2439 }, 3200);
2440 }
2441 }
2442
2443 const protoDecayTimers = {};
2444 function setProtoStreamingInPlace(proto) {
2445 const card = document.getElementById('proto-card-' + proto);
2446 const beacon = document.getElementById('proto-beacon-' + proto);
2447 if (card && beacon) {
2448 card.className = 'proto-card active-stream';
2449 beacon.className = 'proto-beacon beacon-streaming';
2450
2451 if (protoDecayTimers[proto]) {
2452 clearTimeout(protoDecayTimers[proto]);
2453 }
2454 protoDecayTimers[proto] = setTimeout(() => {
2455 delete protoDecayTimers[proto];
2456 card.className = 'proto-card';
2457 beacon.className = 'proto-beacon beacon-ready';
2458 }, 3200);
2459 }
2460 }
2461
2462 let fetchMetricsTimer = null;
2463 function scheduleFetchMetrics() {
2464 if (!fetchMetricsTimer) {
2465 fetchMetricsTimer = setTimeout(() => {
2466 fetchMetricsTimer = null;
2467 fetchMetrics();
2468 }, 400);
2469 }
2470 }
2471
2472 // Connect Server-Sent Events (SSE)
2473 function setupSSE() {
2474 const evtSource = new EventSource('/api/v1/live/events');
2475
2476 evtSource.onopen = function() {
2477 document.getElementById('connPulse').style.background = 'var(--green)';
2478 document.getElementById('connPulse').style.boxShadow = '0 0 8px var(--green)';
2479 document.getElementById('connText').textContent = 'STREAM CONNECTED';
2480 };
2481
2482 evtSource.onerror = function() {
2483 document.getElementById('connPulse').style.background = 'var(--crimson)';
2484 document.getElementById('connPulse').style.boxShadow = '0 0 8px var(--crimson)';
2485 document.getElementById('connText').textContent = 'RECONNECTING...';
2486 };
2487
2488 function onSSEEvent(e) {
2489 try {
2490 const event = JSON.parse(e.data);
2491 handleLiveEvent(event);
2492 } catch (err) {
2493 console.error("SSE parse error:", err);
2494 }
2495 }
2496
2497 evtSource.onmessage = onSSEEvent;
2498 const eventTypes = [
2499 'heartbeat', 'rate_tick', 'chunk_received', 'chunk_progress',
2500 'session_complete', 'probe_received', 'report_uploaded', 'connection_open',
2501 'dashboard_reset'
2502 ];
2503 eventTypes.forEach(type => {
2504 evtSource.addEventListener(type, onSSEEvent);
2505 });
2506
2507 // Periodic fallback polling every 3 seconds to guarantee freshness
2508 setInterval(fetchMetrics, 3000);
2509 }
2510
2511 function handleLiveEvent(evt) {
2512 if (!evt) return;
2513
2514 if (evt.type === 'dashboard_reset') {
2515 handleResetEvent(evt.data);
2516 return;
2517 }
2518
2519 if (evt.type === 'heartbeat' && evt.data) {
2520 const state = evt.data;
2521 updateDashboardState(state);
2522 } else if (evt.type === 'rate_tick' && evt.data) {
2523 const tick = evt.data;
2524 if (tick.instant_throughput_mbs !== undefined) {
2525 document.getElementById('dialThroughputVal').textContent = tick.instant_throughput_mbs.toFixed(2);
2526 setDialProgress('dialThroughputProg', tick.instant_throughput_mbs, MAX_THROUGHPUT);
2527 }
2528 if (tick.active_streams !== undefined) {
2529 document.getElementById('dialConcVal').textContent = tick.active_streams;
2530 setDialProgress('dialConcProg', tick.active_streams, MAX_CONCURRENCY);
2531 }
2532 } else if ((evt.type === 'chunk_progress' || evt.type === 'chunk_received') && evt.data) {
2533 const chunk = evt.data;
2534 if (chunk.instant_mbs !== undefined) {
2535 document.getElementById('dialThroughputVal').textContent = chunk.instant_mbs.toFixed(2);
2536 setDialProgress('dialThroughputProg', chunk.instant_mbs, MAX_THROUGHPUT);
2537 }
2538 if (chunk.session_id) {
2539 const idx = lastActiveSessions.findIndex(s => s.session_id === chunk.session_id);
2540 if (idx >= 0) {
2541 lastActiveSessions[idx] = Object.assign(lastActiveSessions[idx], chunk);
2542 } else {
2543 lastActiveSessions.push(chunk);
2544 }
2545 const existingRow = document.getElementById('stream-' + chunk.session_id);
2546 if (existingRow) {
2547 updateStreamRowInPlace(existingRow, chunk);
2548 } else {
2549 renderActiveStreams(lastActiveSessions, lastRecentSessions);
2550 }
2551 }
2552 const portNum = chunk.port || PROTO_TO_PORT[chunk.protocol] || 0;
2553 if (portNum > 0) {
2554 setPortReceivingInPlace(portNum);
2555 }
2556 if (chunk.protocol) {
2557 setProtoStreamingInPlace(chunk.protocol);
2558 }
2559 scheduleFetchMetrics();
2560 } else if (evt.type === 'session_complete') {
2561 if (evt.data && evt.data.session_id) {
2562 const sc = evt.data;
2563 lastActiveSessions = lastActiveSessions.filter(s => s.session_id !== sc.session_id);
2564 lastRecentSessions = [sc, ...lastRecentSessions.filter(s => s.session_id !== sc.session_id)].slice(0, 100);
2565 renderActiveStreams(lastActiveSessions, lastRecentSessions);
2566 }
2567 fetchMetrics();
2568 } else if (evt.type === 'connection_open' || evt.type === 'probe_received' || evt.type === 'report_uploaded') {
2569 fetchMetrics();
2570 }
2571
2572 if (evt.message && evt.type !== 'heartbeat' && evt.type !== 'rate_tick') {
2573 let lvl = 'INFO';
2574 if (evt.type === 'chunk_progress' || evt.type === 'chunk_received') lvl = 'EXFIL';
2575 else if (evt.type === 'session_complete') {
2576 const v = evt.verdict || (evt.data && evt.data.verdict) || '';
2577 lvl = (v.indexOf('BLOCKED') !== -1) ? 'ALERT' : 'SUCCESS';
2578 }
2579 else if (evt.type === 'probe_received') lvl = 'WARN';
2580 else if (evt.type === 'connection_open') lvl = 'INFO';
2581
2582 addLogToTerminal({
2583 timestamp: evt.timestamp ? (evt.timestamp.length > 12 ? evt.timestamp.substring(11, 23) : evt.timestamp) : '',
2584 level: lvl,
2585 zone: evt.network_zone,
2586 host: evt.client_host,
2587 protocol: evt.protocol,
2588 message: evt.message
2589 });
2590 }
2591 }
2592
2593 function updateDashboardState(state) {
2594 if (!state) return;
2595
2596 const g = state.global || {};
2597
2598 // 1. Throughput Dial
2599 const throughput = g.instant_throughput_mbs || 0;
2600 document.getElementById('dialThroughputVal').textContent = throughput.toFixed(2);
2601 setDialProgress('dialThroughputProg', throughput, MAX_THROUGHPUT);
2602 document.getElementById('dialPeakThroughput').textContent = `Peak: ${(g.peak_throughput_mbs || 0).toFixed(2)} MB/s`;
2603
2604 // 2. Volume Dial
2605 const volumeFormatted = g.smuggled_formatted || '0 B';
2606 document.getElementById('dialVolumeVal').textContent = volumeFormatted;
2607 setDialProgress('dialVolumeProg', (g.total_bytes_smuggled || 0) % (1024 * 1024 * 1024), 1024 * 1024 * 1024);
2608 document.getElementById('dialTotalChunks').textContent = `${g.total_chunks || 0} Chunks Assembled`;
2609
2610 // 3. EEI Dial
2611 const eei = g.global_eei_score || 0;
2612 document.getElementById('dialEEIVal').textContent = `${eei.toFixed(0)}%`;
2613 document.getElementById('dialEEIPosture').textContent = (g.eei_posture || 'HARDENED').toUpperCase();
2614 setDialProgress('dialEEIProg', eei, 100.0);
2615 updateEEIColor(eei);
2616
2617 // 4. Concurrency Dial
2618 const activeCount = g.active_streams || 0;
2619 document.getElementById('dialConcVal').textContent = activeCount;
2620 setDialProgress('dialConcProg', activeCount, MAX_CONCURRENCY);
2621 document.getElementById('dialTotalSessions').textContent = `${g.total_sessions || 0} Total Sessions`;
2622
2623 // Active & Recent Streams
2624 lastActiveSessions = state.active_sessions || [];
2625 lastRecentSessions = state.recent_sessions || [];
2626 renderActiveStreams(lastActiveSessions, lastRecentSessions);
2627
2628 // Protocols
2629 if (state.protocols) {
2630 renderProtocols(state.protocols);
2631 }
2632
2633 // Monitored Ports Matrix
2634 if (state.ports) {
2635 renderPorts(state.ports);
2636 }
2637
2638 // Reports
2639 if (state.reports) {
2640 renderReports(state.reports);
2641 }
2642 }
2643
2644 function fetchMetrics() {
2645 fetch('/api/v1/live/metrics')
2646 .then(res => res.json())
2647 .then(data => {
2648 updateDashboardState(data);
2649 if (data.recent_logs && data.recent_logs.length > 0) {
2650 data.recent_logs.forEach(addLogToTerminal);
2651 }
2652 })
2653 .catch(err => console.error("Failed to fetch initial metrics:", err));
2654 }
2655
2656 function updateLiveClock() {
2657 const clockEl = document.getElementById('liveClock');
2658 if (!clockEl) return;
2659 const now = new Date();
2660 const h = String(now.getUTCHours()).padStart(2, '0');
2661 const m = String(now.getUTCMinutes()).padStart(2, '0');
2662 const s = String(now.getUTCSeconds()).padStart(2, '0');
2663 clockEl.textContent = `${h}:${m}:${s} UTC`;
2664 }
2665
2666 // Initial Load
2667 window.addEventListener('DOMContentLoaded', () => {
2668 updateLiveClock();
2669 setInterval(updateLiveClock, 1000);
2670 initTerminalScrollListener();
2671 fetchMetrics();
2672 setupSSE();
2673 updateGeneratedCommand();
2674 });
2675 </script>
2676</body>
2677</html>