● 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 / metrics.go
← Back to directory
787 lines · 21.8 KB
Raw
1package live
2
3import (
4 "fmt"
5 "math"
6 "net"
7 "sort"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/shehzade/contraband/pkg/common"
13)
14
15// GlobalMetrics aggregates top-level dial and gauge values.
16type GlobalMetrics struct {
17 InstantThroughputMBs float64 `json:"instant_throughput_mbs"`
18 PeakThroughputMBs float64 `json:"peak_throughput_mbs"`
19 TotalBytesSmuggled int64 `json:"total_bytes_smuggled"`
20 SmuggledFormatted string `json:"smuggled_formatted"`
21 ActiveStreams int `json:"active_streams"`
22 GlobalEEIScore float64 `json:"global_eei_score"`
23 EEIPosture string `json:"eei_posture"`
24 TotalSessions int `json:"total_sessions"`
25 TotalChunks int64 `json:"total_chunks"`
26 TotalCredentials int `json:"total_credentials"`
27 TotalReports int `json:"total_reports"`
28}
29
30// ZoneMetrics tracks activity per network zone (e.g. CORP-LAN, DMZ, CLOUD-VPC).
31type ZoneMetrics struct {
32 ZoneName string `json:"zone_name"`
33 TotalBytes int64 `json:"total_bytes"`
34 BytesFormatted string `json:"bytes_formatted"`
35 ActiveStreams int `json:"active_streams"`
36 TotalSessions int `json:"total_sessions"`
37 SuccessCount int `json:"success_count"`
38 BlockedCount int `json:"blocked_count"`
39 HostCount int `json:"host_count"`
40 Hosts []string `json:"hosts,omitempty"`
41}
42
43// ProtocolStatus tracks state and volume for each of the 20 smuggling protocols.
44type ProtocolStatus struct {
45 Protocol string `json:"protocol"`
46 Status string `json:"status"` // "streaming", "ready", "blocked", "idle"
47 TotalBytes int64 `json:"total_bytes"`
48 BytesFormatted string `json:"bytes_formatted"`
49 TotalChunks int64 `json:"total_chunks"`
50 ActiveStreams int `json:"active_streams"`
51 LastActive time.Time `json:"last_active"`
52}
53
54// TerminalLogEntry represents a live colorized log message.
55type TerminalLogEntry struct {
56 ID string `json:"id,omitempty"`
57 Timestamp string `json:"timestamp"` // HH:MM:SS.mmm
58 TimestampMS float64 `json:"timestamp_ms"`
59 Level string `json:"level"` // "INFO", "SUCCESS", "WARN", "ALERT", "EXFIL"
60 Zone string `json:"zone"`
61 Host string `json:"host"`
62 Protocol string `json:"protocol"`
63 Message string `json:"message"`
64}
65
66// MetricsState represents the entire live state snapshot for the dashboard.
67type MetricsState struct {
68 Global GlobalMetrics `json:"global"`
69 Active []SessionProgressData `json:"active_sessions"`
70 Recent []SessionCompleteData `json:"recent_sessions"`
71 Zones map[string]ZoneMetrics `json:"zones"`
72 Protocols map[string]ProtocolStatus `json:"protocols"`
73 Ports []PortStatus `json:"ports"`
74 Logs []TerminalLogEntry `json:"recent_logs"`
75 ReportSummaries []ReportUploadData `json:"reports"`
76}
77
78// MetricsTracker aggregates all metrics with thread safety and sliding window rates.
79type MetricsTracker struct {
80 mu sync.RWMutex
81
82 global GlobalMetrics
83 active map[string]*SessionProgressData
84 recent []SessionCompleteData
85 zones map[string]*ZoneMetrics
86 protocols map[string]*ProtocolStatus
87 ports map[int]*PortStatus
88 logs []TerminalLogEntry
89 reports []ReportUploadData
90 zoneHosts map[string]map[string]bool
91 knownZones map[string]string
92 knownHosts map[string]string
93 totalProbes int
94 successProbes int
95 blockedProbes int
96 logSeq int64
97
98 // Sliding window for instant throughput calculation
99 rateMu sync.Mutex
100 windowBytes int64
101 windowStart time.Time
102 lastSampleMBs float64
103}
104
105// All standard 20 protocols
106var standardProtocols = []string{
107 common.ProtoHTTP1, common.ProtoHTTP2, common.ProtoHTTP3, common.ProtoWebSocket,
108 common.ProtoGRPC, common.ProtoWebDAV, common.ProtoDNSA, common.ProtoDNSTXT,
109 common.ProtoDoH, common.ProtoDoT, common.ProtoFTP, common.ProtoFTPS,
110 common.ProtoSFTP, common.ProtoSMTP, common.ProtoSMBTCP, common.ProtoSMBQUIC,
111 common.ProtoAWSS3, common.ProtoGCPGCS, common.ProtoAzureBlob,
112 common.ProtoICMP, common.ProtoNTP, common.ProtoSyslog, common.ProtoMQTT,
113}
114
115// NewMetricsTracker initializes metrics containers, standard protocols, and monitored ports.
116func NewMetricsTracker() *MetricsTracker {
117 mt := &MetricsTracker{
118 active: make(map[string]*SessionProgressData),
119 recent: make([]SessionCompleteData, 0, 100),
120 zones: make(map[string]*ZoneMetrics),
121 protocols: make(map[string]*ProtocolStatus),
122 ports: make(map[int]*PortStatus),
123 logs: make([]TerminalLogEntry, 0, 200),
124 reports: make([]ReportUploadData, 0, 50),
125 zoneHosts: make(map[string]map[string]bool),
126 knownZones: make(map[string]string),
127 knownHosts: make(map[string]string),
128 windowStart: time.Now(),
129 global: GlobalMetrics{
130 EEIPosture: "Hardened",
131 },
132 }
133
134 for _, p := range standardProtocols {
135 mt.protocols[p] = &ProtocolStatus{
136 Protocol: p,
137 Status: "ready",
138 BytesFormatted: "0 B",
139 }
140 }
141
142 standardPorts := []struct {
143 port int
144 proto string
145 serv string
146 }{
147 {21, "TCP", "FTP"},
148 {22, "TCP", "SSH/SFTP"},
149 {25, "TCP", "SMTP"},
150 {53, "UDP", "DNS"},
151 {80, "TCP", "HTTP"},
152 {123, "UDP", "NTP"},
153 {443, "TCP", "HTTPS"},
154 {445, "TCP", "SMB"},
155 {465, "TCP", "SMTPS"},
156 {514, "UDP", "Syslog"},
157 {587, "TCP", "SMTP-Sub"},
158 {853, "TCP", "DoT"},
159 {990, "TCP", "FTPS"},
160 {1883, "TCP", "MQTT"},
161 {4430, "UDP", "SMB-QUIC"},
162 {6514, "TCP", "Syslog-TLS"},
163 {8080, "TCP", "HTTP-Alt"},
164 {8443, "TCP", "HTTPS-Alt"},
165 {8883, "TCP", "MQTT-TLS"},
166 {50051, "TCP", "gRPC"},
167 }
168 for _, sp := range standardPorts {
169 mt.ports[sp.port] = &PortStatus{
170 Port: sp.port,
171 Protocol: sp.proto,
172 Service: sp.serv,
173 Status: "listening",
174 }
175 }
176
177 return mt
178}
179
180// FormatBytes formats byte counts into human-readable strings.
181func FormatBytes(b int64) string {
182 const unit = 1024
183 if b < unit {
184 return fmt.Sprintf("%d B", b)
185 }
186 div, exp := int64(unit), 0
187 for n := b / unit; n >= unit; n /= unit {
188 div *= unit
189 exp++
190 }
191 return fmt.Sprintf("%.2f %cB", float64(b)/float64(div), "KMGTPE"[exp])
192}
193
194// FormatTimestampMS formats current time as HH:MM:SS.mmm.
195func FormatTimestampMS(t time.Time) string {
196 return fmt.Sprintf("%02d:%02d:%02d.%03d",
197 t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1000000)
198}
199
200// ProtoToPort maps standardized protocol strings to standard network listener ports.
201func ProtoToPort(proto string) int {
202 switch proto {
203 case common.ProtoHTTP1, "http1", "http", common.ProtoWebDAV, common.ProtoWebSocket, "websocket", "ws":
204 return 80
205 case common.ProtoHTTP2, "http2", "https", common.ProtoHTTP3, "http3", "quic", common.ProtoAWSS3, common.ProtoGCPGCS, common.ProtoAzureBlob:
206 return 443
207 case common.ProtoDNSA, common.ProtoDNSTXT, "dns":
208 return 53
209 case common.ProtoFTP:
210 return 21
211 case common.ProtoFTPS:
212 return 990
213 case common.ProtoSFTP:
214 return 22
215 case common.ProtoSMTP:
216 return 25
217 case common.ProtoSMBFile, common.ProtoSMBHash, "smb":
218 return 445
219 case common.ProtoSMBQUIC:
220 return 4430
221 case common.ProtoNTPTime, "ntp":
222 return 123
223 case common.ProtoSyslog:
224 return 514
225 case common.ProtoMQTT:
226 return 1883
227 case common.ProtoGRPC, "grpc":
228 return 50051
229 case common.ProtoDoT, "dot":
230 return 853
231 case common.ProtoDoH, "doh":
232 return 8443
233 default:
234 return 0
235 }
236}
237
238// ClassifyNetworkZone determines zone/subnet classification from an IP or hostname.
239func ClassifyNetworkZone(clientIP string) string {
240 if clientIP == "" {
241 return "Default Zone"
242 }
243 ip := net.ParseIP(clientIP)
244 if ip == nil {
245 lower := strings.ToLower(clientIP)
246 if strings.Contains(lower, "dmz") {
247 return "DMZ"
248 }
249 if strings.Contains(lower, "cloud") || strings.Contains(lower, "vpc") || strings.Contains(lower, "aws") || strings.Contains(lower, "gcp") {
250 return "Cloud VPC"
251 }
252 if strings.Contains(lower, "branch") {
253 return "Branch Office"
254 }
255 return "Corporate LAN"
256 }
257 if ip.IsLoopback() {
258 return "Localhost"
259 }
260 if ip4 := ip.To4(); ip4 != nil {
261 switch {
262 case ip4[0] == 10:
263 return "Corporate LAN"
264 case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
265 return "DMZ"
266 case ip4[0] == 192 && ip4[1] == 168:
267 return "Branch Office"
268 case ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127:
269 return "Cloud VPC"
270 default:
271 return "Cloud VPC"
272 }
273 }
274 return "Cloud VPC"
275}
276
277// RecordChunk updates active stream, protocol, zone, and global metrics.
278func (mt *MetricsTracker) RecordChunk(p SessionProgressData, chunkSize int64) {
279 mt.mu.Lock()
280 defer mt.mu.Unlock()
281
282 // Update sliding window bytes for throughput
283 mt.rateMu.Lock()
284 mt.windowBytes += chunkSize
285 mt.rateMu.Unlock()
286
287 // Remember client's explicit zone and hostname if provided
288 if p.ClientIP != "" {
289 if p.NetworkZone != "" && p.NetworkZone != "DEFAULT" && p.NetworkZone != "Corporate LAN" && p.NetworkZone != "Default Zone" {
290 mt.knownZones[p.ClientIP] = p.NetworkZone
291 }
292 if p.ClientHost != "" && p.ClientHost != p.ClientIP {
293 mt.knownHosts[p.ClientIP] = p.ClientHost
294 }
295 }
296
297 // Auto-classify zone if missing or default, prioritizing learned zone for this client IP
298 if p.NetworkZone == "" || p.NetworkZone == "DEFAULT" || p.NetworkZone == "Default Zone" {
299 if learnedZone, ok := mt.knownZones[p.ClientIP]; ok && learnedZone != "" {
300 p.NetworkZone = learnedZone
301 } else {
302 p.NetworkZone = ClassifyNetworkZone(p.ClientIP)
303 }
304 }
305
306 // Populate client host if missing, prioritizing learned host
307 if p.ClientHost == "" {
308 if learnedHost, ok := mt.knownHosts[p.ClientIP]; ok && learnedHost != "" {
309 p.ClientHost = learnedHost
310 } else if p.ClientIP != "" {
311 p.ClientHost = p.ClientIP
312 }
313 }
314
315 // Update active session
316 sess, exists := mt.active[p.SessionID]
317 if !exists {
318 pCopy := p
319 sess = &pCopy
320 mt.active[p.SessionID] = sess
321 mt.global.ActiveStreams = len(mt.active)
322 } else {
323 sess.Sequence = p.Sequence
324 sess.TotalChunks = p.TotalChunks
325 sess.BytesTransferred = p.BytesTransferred
326 sess.TotalBytes = p.TotalBytes
327 sess.ProgressPct = p.ProgressPct
328 sess.InstantMBs = p.InstantMBs
329 sess.RollingHash = p.RollingHash
330 sess.ElapsedMS = p.ElapsedMS
331 sess.NetworkZone = p.NetworkZone
332 if sess.ClientHost == "" && p.ClientHost != "" {
333 sess.ClientHost = p.ClientHost
334 }
335 if sess.ClientIP == "" && p.ClientIP != "" {
336 sess.ClientIP = p.ClientIP
337 }
338 if sess.Protocol == "" && p.Protocol != "" {
339 sess.Protocol = p.Protocol
340 }
341 }
342
343 // Update global counts
344 mt.global.TotalBytesSmuggled += chunkSize
345 mt.global.SmuggledFormatted = FormatBytes(mt.global.TotalBytesSmuggled)
346 mt.global.TotalChunks++
347
348 // Update zone
349 zoneName := p.NetworkZone
350 if zoneName == "" {
351 zoneName = "Default Zone"
352 }
353 zm, zExists := mt.zones[zoneName]
354 if !zExists {
355 zm = &ZoneMetrics{ZoneName: zoneName, BytesFormatted: "0 B"}
356 mt.zones[zoneName] = zm
357 mt.zoneHosts[zoneName] = make(map[string]bool)
358 }
359 zm.TotalBytes += chunkSize
360 zm.BytesFormatted = FormatBytes(zm.TotalBytes)
361 if p.ClientHost != "" {
362 mt.zoneHosts[zoneName][p.ClientHost] = true
363 } else if p.ClientIP != "" {
364 mt.zoneHosts[zoneName][p.ClientIP] = true
365 } else {
366 mt.zoneHosts[zoneName]["host-unknown"] = true
367 }
368 zm.HostCount = len(mt.zoneHosts[zoneName])
369
370 // Recount active streams for zone
371 activeInZone := 0
372 for _, act := range mt.active {
373 if act.NetworkZone == zoneName {
374 activeInZone++
375 }
376 }
377 zm.ActiveStreams = activeInZone
378
379 // Update protocol
380 proto := p.Protocol
381 if ps, ok := mt.protocols[proto]; ok {
382 ps.TotalBytes += chunkSize
383 ps.BytesFormatted = FormatBytes(ps.TotalBytes)
384 ps.TotalChunks++
385 ps.Status = "streaming"
386 ps.LastActive = time.Now()
387 // Count active streams for this protocol
388 activeInProto := 0
389 for _, act := range mt.active {
390 if act.Protocol == proto {
391 activeInProto++
392 }
393 }
394 ps.ActiveStreams = activeInProto
395 }
396
397 // Update monitored port status
398 port := p.Port
399 if port <= 0 {
400 port = ProtoToPort(p.Protocol)
401 }
402 if port > 0 {
403 if ps, ok := mt.ports[port]; ok {
404 ps.Status = "receiving"
405 ps.BytesRecv += chunkSize
406 ps.LastActive = time.Now()
407 }
408 }
409}
410
411// RecordSessionComplete finalizes an active session.
412func (mt *MetricsTracker) RecordSessionComplete(sc SessionCompleteData) {
413 mt.mu.Lock()
414 defer mt.mu.Unlock()
415
416 delete(mt.active, sc.SessionID)
417 mt.global.ActiveStreams = len(mt.active)
418 mt.global.TotalSessions++
419
420 if sc.ClientIP != "" {
421 if sc.NetworkZone != "" && sc.NetworkZone != "DEFAULT" && sc.NetworkZone != "Corporate LAN" && sc.NetworkZone != "Default Zone" {
422 mt.knownZones[sc.ClientIP] = sc.NetworkZone
423 }
424 if sc.ClientHost != "" && sc.ClientHost != sc.ClientIP {
425 mt.knownHosts[sc.ClientIP] = sc.ClientHost
426 }
427 }
428
429 if sc.NetworkZone == "" || sc.NetworkZone == "DEFAULT" || sc.NetworkZone == "Default Zone" {
430 if learnedZone, ok := mt.knownZones[sc.ClientIP]; ok && learnedZone != "" {
431 sc.NetworkZone = learnedZone
432 } else {
433 sc.NetworkZone = ClassifyNetworkZone(sc.ClientIP)
434 }
435 }
436
437 if sc.ClientHost == "" {
438 if learnedHost, ok := mt.knownHosts[sc.ClientIP]; ok && learnedHost != "" {
439 sc.ClientHost = learnedHost
440 } else if sc.ClientIP != "" {
441 sc.ClientHost = sc.ClientIP
442 }
443 }
444
445 // Append to recent sessions (keep max 100)
446 mt.recent = append([]SessionCompleteData{sc}, mt.recent...)
447 if len(mt.recent) > 100 {
448 mt.recent = mt.recent[:100]
449 }
450
451 // Update zone stats
452 zoneName := sc.NetworkZone
453 if zoneName == "" {
454 zoneName = "Default Zone"
455 }
456 if zm, ok := mt.zones[zoneName]; ok {
457 zm.TotalSessions++
458 if sc.Verdict.IsSuccess() {
459 zm.SuccessCount++
460 } else {
461 zm.BlockedCount++
462 }
463 activeInZone := 0
464 for _, act := range mt.active {
465 if act.NetworkZone == zoneName {
466 activeInZone++
467 }
468 }
469 zm.ActiveStreams = activeInZone
470 }
471
472 // Update protocol status
473 if ps, ok := mt.protocols[sc.Protocol]; ok {
474 activeInProto := 0
475 for _, act := range mt.active {
476 if act.Protocol == sc.Protocol {
477 activeInProto++
478 }
479 }
480 ps.ActiveStreams = activeInProto
481 if activeInProto == 0 {
482 if sc.Verdict.IsBlocked() {
483 ps.Status = "blocked"
484 } else {
485 ps.Status = "ready"
486 }
487 }
488 }
489
490 // Update port status
491 port := sc.Port
492 if port <= 0 {
493 port = ProtoToPort(sc.Protocol)
494 }
495 if port > 0 {
496 if ps, ok := mt.ports[port]; ok {
497 if sc.Verdict.IsBlocked() {
498 ps.Status = "blocked"
499 ps.LastActive = time.Now()
500 } else {
501 // Keep LastActive fresh so the 3-second decay loop transitions status back to listening gracefully,
502 // ensuring UI can visibly display receiving indicator.
503 ps.LastActive = time.Now()
504 }
505 }
506 }
507
508 // Add log entry
509 logLevel := "SUCCESS"
510 if sc.Verdict.IsBlocked() {
511 logLevel = "ALERT"
512 }
513 now := time.Now()
514 mt.addLogLocked(TerminalLogEntry{
515 Timestamp: FormatTimestampMS(now),
516 TimestampMS: float64(now.UnixNano()) / 1e6,
517 Level: logLevel,
518 Zone: sc.NetworkZone,
519 Host: sc.ClientHost,
520 Protocol: sc.Protocol,
521 Message: fmt.Sprintf("[%s] Session %s complete (%s transferred in %.2fms, avg %.2f MB/s, verdict: %s)", sc.Protocol, SafePrefix(sc.SessionID, 8), FormatBytes(sc.TotalBytes), sc.DurationMS, sc.AverageMBs, sc.Verdict),
522 })
523}
524
525// RecordProbe records an individual probe result, updating EEI risk score and port status.
526func (mt *MetricsTracker) RecordProbe(p common.ProbeResult) {
527 mt.mu.Lock()
528 defer mt.mu.Unlock()
529
530 mt.totalProbes++
531 if p.Verdict.IsSuccess() {
532 mt.successProbes++
533 } else if p.Verdict.IsBlocked() {
534 mt.blockedProbes++
535 }
536
537 if mt.totalProbes > 0 {
538 score := (float64(mt.successProbes) / float64(mt.totalProbes)) * 100.0
539 mt.global.GlobalEEIScore = math.Round(score*100) / 100
540 if score >= 75.0 {
541 mt.global.EEIPosture = "Critical"
542 } else if score >= 40.0 {
543 mt.global.EEIPosture = "Moderate"
544 } else {
545 mt.global.EEIPosture = "Hardened"
546 }
547 }
548
549 // Update port status
550 port := ProtoToPort(p.Protocol)
551 if port == 0 && p.TargetPort > 0 {
552 port = p.TargetPort
553 }
554 if port > 0 {
555 if ps, ok := mt.ports[port]; ok {
556 ps.ProbesRecv++
557 ps.BytesRecv += p.BytesSent + p.BytesRecv
558 ps.LastActive = time.Now()
559 if p.Verdict.IsSuccess() {
560 ps.Status = "reflection"
561 } else if p.Verdict.IsBlocked() {
562 ps.Status = "blocked"
563 }
564 }
565 }
566
567 // Terminal log
568 now := time.Now()
569 level := "INFO"
570 if p.Verdict.IsSuccess() {
571 level = "EXFIL"
572 } else if p.Verdict.IsBlocked() {
573 level = "ALERT"
574 }
575 mt.addLogLocked(TerminalLogEntry{
576 Timestamp: FormatTimestampMS(now),
577 TimestampMS: float64(now.UnixNano()) / 1e6,
578 Level: level,
579 Zone: ClassifyNetworkZone(p.TargetHost),
580 Host: p.TargetHost,
581 Protocol: p.Protocol,
582 Message: fmt.Sprintf("[%s] Probe recorded port %d: %s (sent %s)", p.Protocol, port, p.Verdict, FormatBytes(p.BytesSent)),
583 })
584}
585
586// RecordReportUpload stores synced assessment report metadata.
587func (mt *MetricsTracker) RecordReportUpload(r ReportUploadData) {
588 mt.mu.Lock()
589 defer mt.mu.Unlock()
590
591 mt.reports = append([]ReportUploadData{r}, mt.reports...)
592 if len(mt.reports) > 50 {
593 mt.reports = mt.reports[:50]
594 }
595 mt.global.TotalReports = len(mt.reports)
596
597 // Update Global EEI score to the latest or average report score
598 mt.global.GlobalEEIScore = r.EEIScore
599 mt.global.EEIPosture = r.Posture
600
601 now := time.Now()
602 mt.addLogLocked(TerminalLogEntry{
603 Timestamp: FormatTimestampMS(now),
604 TimestampMS: float64(now.UnixNano()) / 1e6,
605 Level: "INFO",
606 Zone: r.NetworkZone,
607 Host: r.ClientHost,
608 Protocol: "ASSESSMENT",
609 Message: fmt.Sprintf("Client report synced: %s (EEI: %.2f%%, %d probes, %d succeeded, %d blocked)", SafePrefix(r.ReportID, 8), r.EEIScore, r.TotalProbes, r.SuccessCount, r.BlockedL4+r.BlockedL7+r.BlockedDLP),
610 })
611}
612
613// AddLog appends a live terminal log entry.
614func (mt *MetricsTracker) AddLog(entry TerminalLogEntry) {
615 mt.mu.Lock()
616 defer mt.mu.Unlock()
617 mt.addLogLocked(entry)
618}
619
620func (mt *MetricsTracker) addLogLocked(entry TerminalLogEntry) {
621 now := time.Now()
622 if entry.Timestamp == "" {
623 entry.Timestamp = FormatTimestampMS(now)
624 }
625 if entry.TimestampMS == 0 {
626 entry.TimestampMS = float64(now.UnixNano()) / 1e6
627 }
628 if entry.ID == "" {
629 mt.logSeq++
630 entry.ID = fmt.Sprintf("log-%d", mt.logSeq)
631 }
632 mt.logs = append(mt.logs, entry)
633 if len(mt.logs) > 200 {
634 mt.logs = mt.logs[len(mt.logs)-200:]
635 }
636}
637
638// Reset clears all active/recent sessions, logs, reports, and zeroes all dials and matrices.
639func (mt *MetricsTracker) Reset() {
640 mt.rateMu.Lock()
641 mt.windowBytes = 0
642 mt.windowStart = time.Now()
643 mt.lastSampleMBs = 0
644 mt.rateMu.Unlock()
645
646 mt.mu.Lock()
647 defer mt.mu.Unlock()
648
649 mt.global = GlobalMetrics{
650 InstantThroughputMBs: 0,
651 PeakThroughputMBs: 0,
652 TotalBytesSmuggled: 0,
653 SmuggledFormatted: "0 B",
654 ActiveStreams: 0,
655 GlobalEEIScore: 0,
656 EEIPosture: "Hardened",
657 TotalSessions: 0,
658 TotalChunks: 0,
659 TotalCredentials: 0,
660 TotalReports: 0,
661 }
662
663 mt.active = make(map[string]*SessionProgressData)
664 mt.recent = make([]SessionCompleteData, 0, 100)
665 mt.zones = make(map[string]*ZoneMetrics)
666 mt.zoneHosts = make(map[string]map[string]bool)
667 mt.reports = make([]ReportUploadData, 0, 50)
668 mt.logs = make([]TerminalLogEntry, 0, 200)
669 mt.totalProbes = 0
670 mt.successProbes = 0
671 mt.blockedProbes = 0
672
673 for _, ps := range mt.protocols {
674 ps.Status = "ready"
675 ps.TotalBytes = 0
676 ps.BytesFormatted = "0 B"
677 ps.TotalChunks = 0
678 ps.ActiveStreams = 0
679 ps.LastActive = time.Time{}
680 }
681
682 for _, ps := range mt.ports {
683 ps.Status = "listening"
684 ps.BytesRecv = 0
685 ps.ProbesRecv = 0
686 ps.LastActive = time.Time{}
687 }
688
689 now := time.Now()
690 mt.addLogLocked(TerminalLogEntry{
691 Timestamp: FormatTimestampMS(now),
692 TimestampMS: float64(now.UnixNano()) / 1e6,
693 Level: "INFO",
694 Message: "Dashboard telemetry reset by operator",
695 })
696}
697
698// TickWindow recalculates instantaneous throughput and decay idle protocol states.
699func (mt *MetricsTracker) TickWindow() {
700 now := time.Now()
701
702 mt.rateMu.Lock()
703 elapsed := now.Sub(mt.windowStart).Seconds()
704 bytes := mt.windowBytes
705 mt.windowBytes = 0
706 mt.windowStart = now
707 mt.rateMu.Unlock()
708
709 var instantMBs float64
710 if elapsed > 0 {
711 instantMBs = (float64(bytes) / (1024 * 1024)) / elapsed
712 }
713
714 mt.mu.Lock()
715 defer mt.mu.Unlock()
716
717 // Smooth or update instant throughput
718 mt.lastSampleMBs = math.Round(instantMBs*100) / 100
719 mt.global.InstantThroughputMBs = mt.lastSampleMBs
720 if mt.lastSampleMBs > mt.global.PeakThroughputMBs {
721 mt.global.PeakThroughputMBs = mt.lastSampleMBs
722 }
723
724 // Decay protocols from streaming to ready if no active streams
725 for _, ps := range mt.protocols {
726 if ps.ActiveStreams == 0 && ps.Status == "streaming" && now.Sub(ps.LastActive) > 3*time.Second {
727 ps.Status = "ready"
728 }
729 }
730
731 // Decay ports from reflection/receiving to listening if inactive for >3s
732 for _, ps := range mt.ports {
733 if (ps.Status == "reflection" || ps.Status == "receiving") && now.Sub(ps.LastActive) > 3*time.Second {
734 ps.Status = "listening"
735 }
736 }
737}
738
739// Snapshot returns a complete, thread-safe deep copy of the current metrics state.
740func (mt *MetricsTracker) Snapshot() MetricsState {
741 mt.mu.RLock()
742 defer mt.mu.RUnlock()
743
744 state := MetricsState{
745 Global: mt.global,
746 Active: make([]SessionProgressData, 0, len(mt.active)),
747 Recent: make([]SessionCompleteData, len(mt.recent)),
748 Zones: make(map[string]ZoneMetrics, len(mt.zones)),
749 Protocols: make(map[string]ProtocolStatus, len(mt.protocols)),
750 Logs: make([]TerminalLogEntry, len(mt.logs)),
751 ReportSummaries: make([]ReportUploadData, len(mt.reports)),
752 }
753
754 for _, a := range mt.active {
755 state.Active = append(state.Active, *a)
756 }
757 copy(state.Recent, mt.recent)
758 copy(state.Logs, mt.logs)
759 copy(state.ReportSummaries, mt.reports)
760
761 for k, v := range mt.zones {
762 zm := *v
763 if hosts, ok := mt.zoneHosts[k]; ok {
764 hostList := make([]string, 0, len(hosts))
765 for h := range hosts {
766 hostList = append(hostList, h)
767 }
768 zm.Hosts = hostList
769 }
770 state.Zones[k] = zm
771 }
772
773 for k, v := range mt.protocols {
774 state.Protocols[k] = *v
775 }
776
777 portsList := make([]PortStatus, 0, len(mt.ports))
778 for _, p := range mt.ports {
779 portsList = append(portsList, *p)
780 }
781 sort.Slice(portsList, func(i, j int) bool {
782 return portsList[i].Port < portsList[j].Port
783 })
784 state.Ports = portsList
785
786 return state
787}