[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● 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)
787 lines · 21.8 KB
| 1 | package live |
| 2 | |
| 3 | import ( |
| 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. |
| 16 | type 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). |
| 31 | type 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. |
| 44 | type 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. |
| 55 | type 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. |
| 67 | type 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. |
| 79 | type 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 |
| 106 | var 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. |
| 116 | func 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. |
| 181 | func 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. |
| 195 | func 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. |
| 201 | func 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. |
| 239 | func 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. |
| 278 | func (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. |
| 412 | func (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. |
| 526 | func (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. |
| 587 | func (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. |
| 614 | func (mt *MetricsTracker) AddLog(entry TerminalLogEntry) { |
| 615 | mt.mu.Lock() |
| 616 | defer mt.mu.Unlock() |
| 617 | mt.addLogLocked(entry) |
| 618 | } |
| 619 | |
| 620 | func (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. |
| 639 | func (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. |
| 699 | func (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. |
| 740 | func (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 | } |