● 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 / hub.go
← Back to directory
402 lines · 11.2 KB
Raw
1package live
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sync"
8 "time"
9
10 "github.com/shehzade/contraband/pkg/common"
11 "github.com/shehzade/contraband/pkg/report"
12)
13
14// StoredReport retains an uploaded client assessment report with its pre-rendered artifacts.
15type StoredReport struct {
16 ID string `json:"id"`
17 Report *report.AssessmentReport `json:"report"`
18 RawHTML []byte `json:"-"`
19 RawJSON []byte `json:"-"`
20 RawSARIF []byte `json:"-"`
21 Remediation []byte `json:"-"`
22 NetworkZone string `json:"network_zone"`
23 ClientHost string `json:"client_host"`
24 ClientIP string `json:"client_ip"`
25 UploadedAt time.Time `json:"uploaded_at"`
26}
27
28// LiveHub coordinates SSE subscribers, real-time metrics, and report storage.
29type LiveHub struct {
30 startOnce sync.Once
31 metrics *MetricsTracker
32 subMu sync.RWMutex
33 subscribers map[chan LiveEvent]bool
34
35 repMu sync.RWMutex
36 reports map[string]*StoredReport
37
38 ctx context.Context
39 cancel context.CancelFunc
40}
41
42// NewLiveHub creates and returns an initialized hub.
43func NewLiveHub() *LiveHub {
44 return &LiveHub{
45 metrics: NewMetricsTracker(),
46 subscribers: make(map[chan LiveEvent]bool),
47 reports: make(map[string]*StoredReport),
48 }
49}
50
51// Start begins background telemetry calculation, rate ticks, and heartbeat broadcasts.
52func (h *LiveHub) Start(ctx context.Context) {
53 h.startOnce.Do(func() {
54 h.ctx, h.cancel = context.WithCancel(ctx)
55
56 go func() {
57 ticker := time.NewTicker(1 * time.Second)
58 defer ticker.Stop()
59
60 for {
61 select {
62 case <-h.ctx.Done():
63 return
64 case <-ticker.C:
65 h.metrics.TickWindow()
66
67 snapshot := h.metrics.Snapshot()
68 now := time.Now()
69 nowMS := float64(now.UnixNano()) / 1e6
70
71 // 1. Broadcast sliding window rate tick
72 h.Broadcast(LiveEvent{
73 ID: fmt.Sprintf("tick-%d", now.UnixMilli()),
74 Type: EventRateTick,
75 Timestamp: now,
76 TimestampMS: nowMS,
77 Message: fmt.Sprintf("Rate tick: %.2f MB/s (peak: %.2f MB/s, streams: %d)", snapshot.Global.InstantThroughputMBs, snapshot.Global.PeakThroughputMBs, snapshot.Global.ActiveStreams),
78 Data: RateTickData{
79 InstantThroughputMBs: snapshot.Global.InstantThroughputMBs,
80 PeakThroughputMBs: snapshot.Global.PeakThroughputMBs,
81 ActiveStreams: snapshot.Global.ActiveStreams,
82 TotalBytes: snapshot.Global.TotalBytesSmuggled,
83 TimestampMS: nowMS,
84 },
85 })
86
87 // 2. Broadcast periodic heartbeat with complete snapshot
88 h.Broadcast(LiveEvent{
89 ID: fmt.Sprintf("hb-%d", now.UnixMilli()),
90 Type: EventHeartbeat,
91 Timestamp: now,
92 TimestampMS: nowMS,
93 Message: "Heartbeat update",
94 Data: snapshot,
95 })
96 }
97 }
98 }()
99 })
100}
101
102// Stop shuts down the hub and disconnects subscribers.
103func (h *LiveHub) Stop() {
104 if h.cancel != nil {
105 h.cancel()
106 }
107 h.subMu.Lock()
108 defer h.subMu.Unlock()
109 for ch := range h.subscribers {
110 close(ch)
111 delete(h.subscribers, ch)
112 }
113}
114
115// Subscribe registers a new SSE event listener.
116func (h *LiveHub) Subscribe() chan LiveEvent {
117 ch := make(chan LiveEvent, 256)
118 h.subMu.Lock()
119 h.subscribers[ch] = true
120 h.subMu.Unlock()
121 return ch
122}
123
124// Unsubscribe removes an SSE event listener.
125func (h *LiveHub) Unsubscribe(ch chan LiveEvent) {
126 h.subMu.Lock()
127 defer h.subMu.Unlock()
128 if _, ok := h.subscribers[ch]; ok {
129 delete(h.subscribers, ch)
130 close(ch)
131 }
132}
133
134// Broadcast sends an event to all subscribers without blocking callers.
135func (h *LiveHub) Broadcast(event LiveEvent) {
136 h.subMu.RLock()
137 defer h.subMu.RUnlock()
138
139 for ch := range h.subscribers {
140 select {
141 case ch <- event:
142 default:
143 // Non-blocking drop for slow consumers to prevent stalling the hub
144 }
145 }
146}
147
148// RecordConnectionOpen registers a new client connection arrival and broadcasts connection_open.
149func (h *LiveHub) RecordConnectionOpen(zone, host, ip, proto string, port int, sessionID string) {
150 now := time.Now()
151 nowMS := float64(now.UnixNano()) / 1e6
152 if zone == "" {
153 zone = ClassifyNetworkZone(ip)
154 }
155 if host == "" {
156 host = ip
157 }
158
159 h.Broadcast(LiveEvent{
160 ID: fmt.Sprintf("conn-%d", now.UnixNano()),
161 Type: EventConnectionOpen,
162 Timestamp: now,
163 TimestampMS: nowMS,
164 SessionID: sessionID,
165 NetworkZone: zone,
166 ClientHost: host,
167 ClientIP: ip,
168 Protocol: proto,
169 Message: fmt.Sprintf("[%s] Connection opened from %s (%s:%d)", proto, host, ip, port),
170 Data: ConnectionOpenData{
171 SessionID: sessionID,
172 NetworkZone: zone,
173 ClientHost: host,
174 ClientIP: ip,
175 Protocol: proto,
176 Port: port,
177 TimestampMS: nowMS,
178 },
179 })
180}
181
182// RecordChunk registers an incoming payload chunk and broadcasts chunk_received.
183func (h *LiveHub) RecordChunk(p SessionProgressData, chunkSize int64) {
184 h.metrics.RecordChunk(p, chunkSize)
185
186 now := time.Now()
187 nowMS := float64(now.UnixNano()) / 1e6
188 h.Broadcast(LiveEvent{
189 ID: fmt.Sprintf("chk-%s-%d", SafePrefix(p.SessionID, 8), p.Sequence),
190 Type: EventChunkReceived,
191 Timestamp: now,
192 TimestampMS: nowMS,
193 SessionID: p.SessionID,
194 NetworkZone: p.NetworkZone,
195 ClientHost: p.ClientHost,
196 ClientIP: p.ClientIP,
197 Protocol: p.Protocol,
198 Message: fmt.Sprintf("[%s] Chunk %d/%d received (%.2f MB/s)", p.Protocol, p.Sequence, p.TotalChunks, p.InstantMBs),
199 Data: p,
200 })
201}
202
203// RecordSessionComplete registers session finalization and broadcasts session_complete.
204func (h *LiveHub) RecordSessionComplete(sc SessionCompleteData) {
205 h.metrics.RecordSessionComplete(sc)
206
207 now := time.Now()
208 nowMS := float64(now.UnixNano()) / 1e6
209 h.Broadcast(LiveEvent{
210 ID: fmt.Sprintf("done-%s", SafePrefix(sc.SessionID, 8)),
211 Type: EventSessionComplete,
212 Timestamp: now,
213 TimestampMS: nowMS,
214 SessionID: sc.SessionID,
215 NetworkZone: sc.NetworkZone,
216 ClientHost: sc.ClientHost,
217 ClientIP: sc.ClientIP,
218 Protocol: sc.Protocol,
219 Message: fmt.Sprintf("[%s] Session completed with verdict %s (%s in %.2fms)", sc.Protocol, sc.Verdict, FormatBytes(sc.TotalBytes), sc.DurationMS),
220 Data: sc,
221 })
222}
223
224// RecordProbe registers an individual reflection/sweep probe result and broadcasts probe_received.
225func (h *LiveHub) RecordProbe(p common.ProbeResult) {
226 h.metrics.RecordProbe(p)
227
228 now := time.Now()
229 nowMS := float64(now.UnixNano()) / 1e6
230 zone := ClassifyNetworkZone(p.TargetHost)
231
232 h.Broadcast(LiveEvent{
233 ID: fmt.Sprintf("prb-%d", now.UnixNano()),
234 Type: EventProbeReceived,
235 Timestamp: now,
236 TimestampMS: nowMS,
237 NetworkZone: zone,
238 ClientHost: p.TargetHost,
239 ClientIP: p.LocalAddr,
240 Protocol: p.Protocol,
241 Message: fmt.Sprintf("[%s] Probe recorded for %s:%d: %s", p.Protocol, p.TargetHost, p.TargetPort, p.Verdict),
242 Data: ProbeData{
243 ProbeID: p.ID,
244 NetworkZone: zone,
245 ClientHost: p.TargetHost,
246 ClientIP: p.LocalAddr,
247 Protocol: p.Protocol,
248 TargetPort: p.TargetPort,
249 Verdict: p.Verdict,
250 TimestampMS: nowMS,
251 },
252 })
253}
254
255// StoreReport saves an uploaded assessment report bundle and broadcasts notification.
256func (h *LiveHub) StoreReport(rep *report.AssessmentReport, rawHTML, rawJSON, rawSARIF, remediation []byte, zone, host, ip string) string {
257 reportID := rep.ID
258 if reportID == "" {
259 reportID = fmt.Sprintf("rep-%d", time.Now().UnixMilli())
260 rep.ID = reportID
261 }
262
263 stored := &StoredReport{
264 ID: reportID,
265 Report: rep,
266 RawHTML: rawHTML,
267 RawJSON: rawJSON,
268 RawSARIF: rawSARIF,
269 Remediation: remediation,
270 NetworkZone: zone,
271 ClientHost: host,
272 ClientIP: ip,
273 UploadedAt: time.Now(),
274 }
275
276 h.repMu.Lock()
277 const maxRetainedReports = 50
278 if len(h.reports) >= maxRetainedReports {
279 for oldID := range h.reports {
280 delete(h.reports, oldID)
281 break
282 }
283 }
284 h.reports[reportID] = stored
285 h.repMu.Unlock()
286
287 summary := ReportUploadData{
288 ReportID: reportID,
289 TargetHost: rep.TargetHost,
290 NetworkZone: zone,
291 ClientHost: host,
292 ClientIP: ip,
293 EEIScore: rep.Summary.EEIScore,
294 Posture: string(rep.Summary.Posture),
295 TotalProbes: rep.Summary.TotalProbes,
296 SuccessCount: rep.Summary.SuccessCount,
297 BlockedL4: rep.Summary.BlockedL4,
298 BlockedL7: rep.Summary.BlockedL7,
299 BlockedDLP: rep.Summary.BlockedDLP,
300 UploadedAt: time.Now().UTC().Format(time.RFC3339),
301 }
302
303 h.metrics.RecordReportUpload(summary)
304
305 now := time.Now()
306 h.Broadcast(LiveEvent{
307 ID: fmt.Sprintf("rep-event-%s", SafePrefix(reportID, 8)),
308 Type: EventReportUploaded,
309 Timestamp: now,
310 TimestampMS: float64(now.UnixNano()) / 1e6,
311 NetworkZone: zone,
312 ClientHost: host,
313 ClientIP: ip,
314 Message: fmt.Sprintf("New Assessment Report uploaded: %s (EEI: %.2f%%)", SafePrefix(reportID, 8), rep.Summary.EEIScore),
315 Data: summary,
316 })
317
318 return reportID
319}
320
321// GetReport retrieves a stored report by ID.
322func (h *LiveHub) GetReport(id string) (*StoredReport, bool) {
323 h.repMu.RLock()
324 defer h.repMu.RUnlock()
325 rep, ok := h.reports[id]
326 return rep, ok
327}
328
329// ListReports returns summaries of all uploaded reports.
330func (h *LiveHub) ListReports() []ReportUploadData {
331 h.repMu.RLock()
332 defer h.repMu.RUnlock()
333
334 list := make([]ReportUploadData, 0, len(h.reports))
335 for _, r := range h.reports {
336 list = append(list, ReportUploadData{
337 ReportID: r.ID,
338 TargetHost: r.Report.TargetHost,
339 NetworkZone: r.NetworkZone,
340 ClientHost: r.ClientHost,
341 ClientIP: r.ClientIP,
342 EEIScore: r.Report.Summary.EEIScore,
343 Posture: string(r.Report.Summary.Posture),
344 TotalProbes: r.Report.Summary.TotalProbes,
345 SuccessCount: r.Report.Summary.SuccessCount,
346 BlockedL4: r.Report.Summary.BlockedL4,
347 BlockedL7: r.Report.Summary.BlockedL7,
348 BlockedDLP: r.Report.Summary.BlockedDLP,
349 UploadedAt: r.UploadedAt.UTC().Format(time.RFC3339),
350 })
351 }
352 return list
353}
354
355// GetMetricsSnapshot returns the current state of metrics.
356func (h *LiveHub) GetMetricsSnapshot() MetricsState {
357 return h.metrics.Snapshot()
358}
359
360// AddLog appends a live terminal log.
361func (h *LiveHub) AddLog(level, zone, host, proto, msg string) {
362 now := time.Now()
363 entry := TerminalLogEntry{
364 Timestamp: FormatTimestampMS(now),
365 TimestampMS: float64(now.UnixNano()) / 1e6,
366 Level: level,
367 Zone: zone,
368 Host: host,
369 Protocol: proto,
370 Message: msg,
371 }
372 h.metrics.AddLog(entry)
373}
374
375// Reset clears all metrics, active sessions, logs, and stored reports, and broadcasts EventDashboardReset.
376func (h *LiveHub) Reset(deletedFiles bool, evidenceDir string) {
377 h.metrics.Reset()
378
379 h.repMu.Lock()
380 h.reports = make(map[string]*StoredReport)
381 h.repMu.Unlock()
382
383 now := time.Now()
384 nowMS := float64(now.UnixNano()) / 1e6
385 h.Broadcast(LiveEvent{
386 ID: fmt.Sprintf("reset-%d", now.UnixMilli()),
387 Type: EventDashboardReset,
388 Timestamp: now,
389 TimestampMS: nowMS,
390 Message: "Dashboard telemetry and metrics reset by operator",
391 Data: DashboardResetData{
392 DeletedFiles: deletedFiles,
393 EvidenceDir: evidenceDir,
394 TimestampMS: nowMS,
395 },
396 })
397}
398
399// MarshalJSON marshals current metrics snapshot for API endpoints.
400func (h *LiveHub) MarshalJSON() ([]byte, error) {
401 return json.Marshal(h.metrics.Snapshot())
402}