● 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 / server.go
← Back to directory
394 lines · 10.6 KB
Raw
1package live
2
3import (
4 "context"
5 _ "embed"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net"
10 "net/http"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/shehzade/contraband/pkg/report"
16)
17
18//go:embed dashboard.html
19var dashboardHTML []byte
20
21// ResetCallback is invoked when an operator triggers a dashboard state reset.
22type ResetCallback func(deleteFiles bool) error
23
24// ResetRequest defines the JSON payload for resetting the dashboard.
25type ResetRequest struct {
26 DeleteFiles bool `json:"delete_files"`
27}
28
29// ResetResponse models the confirmation payload returned by the reset API.
30type ResetResponse struct {
31 Success bool `json:"success"`
32 Message string `json:"message"`
33 DeletedFiles bool `json:"deleted_files"`
34 EvidenceDir string `json:"evidence_dir,omitempty"`
35}
36
37// DashboardServer coordinates the HTTP live dashboard and report center.
38type DashboardServer struct {
39 mu sync.Mutex
40 hub *LiveHub
41 httpServer *http.Server
42 listener net.Listener
43 addr string
44 evidenceDir string
45 resetCallback ResetCallback
46 closed bool
47}
48
49// UploadReportPayload models the payload sent by the client when syncing its report.
50type UploadReportPayload struct {
51 Report *report.AssessmentReport `json:"report"`
52 RawHTML string `json:"raw_html,omitempty"`
53 RawJSON string `json:"raw_json,omitempty"`
54 RawSARIF string `json:"raw_sarif,omitempty"`
55 Remediation string `json:"remediation,omitempty"`
56 NetworkZone string `json:"network_zone,omitempty"`
57 ClientHost string `json:"client_host,omitempty"`
58 ClientIP string `json:"client_ip,omitempty"`
59}
60
61// NewDashboardServer creates and binds a new DashboardServer instance.
62func NewDashboardServer(bindAddr string, hub *LiveHub) (*DashboardServer, error) {
63 if hub == nil {
64 hub = NewLiveHub()
65 }
66
67 if bindAddr == "" {
68 bindAddr = "0.0.0.0:8080"
69 }
70 if !strings.Contains(bindAddr, ":") {
71 bindAddr = ":" + bindAddr
72 }
73
74 ln, err := net.Listen("tcp", bindAddr)
75 if err != nil {
76 return nil, fmt.Errorf("failed to bind live dashboard on %s: %w", bindAddr, err)
77 }
78
79 ds := &DashboardServer{
80 hub: hub,
81 listener: ln,
82 addr: ln.Addr().String(),
83 }
84
85 mux := http.NewServeMux()
86 mux.HandleFunc("/", ds.handleRoot)
87 mux.HandleFunc("/dashboard", ds.handleDashboard)
88 mux.HandleFunc("/api/v1/live/events", ds.handleSSEEvents)
89 mux.HandleFunc("/events", ds.handleSSEEvents)
90 mux.HandleFunc("/api/stream", ds.handleSSEEvents)
91 mux.HandleFunc("/api/v1/live/metrics", ds.handleMetrics)
92 mux.HandleFunc("/metrics", ds.handleMetrics)
93 mux.HandleFunc("/api/metrics", ds.handleMetrics)
94 mux.HandleFunc("/api/v1/reports", ds.handleReports)
95 mux.HandleFunc("/api/v1/reports/", ds.handleReportArtifacts)
96 mux.HandleFunc("/reports", ds.handleDashboard)
97 mux.HandleFunc("/api/v1/live/reset", ds.handleReset)
98 mux.HandleFunc("/api/v1/reset", ds.handleReset)
99
100 ds.httpServer = &http.Server{
101 Handler: mux,
102 ReadTimeout: 15 * time.Second,
103 WriteTimeout: 0, // SSE requires unbounded write timeout
104 IdleTimeout: 60 * time.Second,
105 }
106
107 return ds, nil
108}
109
110// Addr returns the bound TCP address.
111func (ds *DashboardServer) Addr() string {
112 return ds.addr
113}
114
115// Hub returns the underlying LiveHub.
116func (ds *DashboardServer) Hub() *LiveHub {
117 return ds.hub
118}
119
120// SetEvidenceDir sets the directory path for reconstructed payload evidence.
121func (ds *DashboardServer) SetEvidenceDir(dir string) {
122 ds.mu.Lock()
123 defer ds.mu.Unlock()
124 ds.evidenceDir = dir
125}
126
127// SetResetCallback registers a hook executed whenever an operator resets the dashboard.
128func (ds *DashboardServer) SetResetCallback(cb ResetCallback) {
129 ds.mu.Lock()
130 defer ds.mu.Unlock()
131 ds.resetCallback = cb
132}
133
134// Start begins serving HTTP and SSE requests.
135func (ds *DashboardServer) Start(ctx context.Context) error {
136 ds.hub.Start(ctx)
137
138 go func() {
139 <-ctx.Done()
140 _ = ds.Shutdown(context.Background())
141 }()
142
143 err := ds.httpServer.Serve(ds.listener)
144 if err != nil && err != http.ErrServerClosed {
145 return err
146 }
147 return nil
148}
149
150// Shutdown gracefully terminates the dashboard server.
151func (ds *DashboardServer) Shutdown(ctx context.Context) error {
152 ds.mu.Lock()
153 if ds.closed {
154 ds.mu.Unlock()
155 return nil
156 }
157 ds.closed = true
158 ds.mu.Unlock()
159
160 ds.hub.Stop()
161 return ds.httpServer.Shutdown(ctx)
162}
163
164func (ds *DashboardServer) handleRoot(w http.ResponseWriter, r *http.Request) {
165 if r.URL.Path != "/" {
166 http.NotFound(w, r)
167 return
168 }
169 http.Redirect(w, r, "/dashboard", http.StatusTemporaryRedirect)
170}
171
172func (ds *DashboardServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
173 w.Header().Set("Content-Type", "text/html; charset=utf-8")
174 w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
175 w.WriteHeader(http.StatusOK)
176 _, _ = w.Write(dashboardHTML)
177}
178
179func (ds *DashboardServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
180 w.Header().Set("Content-Type", "application/json")
181 w.Header().Set("Cache-Control", "no-cache")
182 snapshot := ds.hub.GetMetricsSnapshot()
183 _ = json.NewEncoder(w).Encode(snapshot)
184}
185
186func (ds *DashboardServer) handleSSEEvents(w http.ResponseWriter, r *http.Request) {
187 flusher, ok := w.(http.Flusher)
188 if !ok {
189 http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
190 return
191 }
192
193 w.Header().Set("Content-Type", "text/event-stream")
194 w.Header().Set("Cache-Control", "no-cache")
195 w.Header().Set("Connection", "keep-alive")
196 w.Header().Set("Access-Control-Allow-Origin", "*")
197
198 ch := ds.hub.Subscribe()
199 defer ds.hub.Unsubscribe(ch)
200
201 // Send initial snapshot on connect
202 initialState := ds.hub.GetMetricsSnapshot()
203 initData, _ := json.Marshal(LiveEvent{
204 ID: fmt.Sprintf("init-%d", time.Now().UnixMilli()),
205 Type: EventHeartbeat,
206 Timestamp: time.Now(),
207 TimestampMS: float64(time.Now().UnixNano()) / 1e6,
208 Message: "Initial snapshot",
209 Data: initialState,
210 })
211 _, _ = fmt.Fprintf(w, "event: heartbeat\ndata: %s\n\n", initData)
212 flusher.Flush()
213
214 ctx := r.Context()
215 for {
216 select {
217 case <-ctx.Done():
218 return
219 case event, open := <-ch:
220 if !open {
221 return
222 }
223 data, err := json.Marshal(event)
224 if err != nil {
225 continue
226 }
227 _, _ = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, data)
228 flusher.Flush()
229 }
230 }
231}
232
233func (ds *DashboardServer) handleReports(w http.ResponseWriter, r *http.Request) {
234 switch r.Method {
235 case http.MethodGet:
236 w.Header().Set("Content-Type", "application/json")
237 reports := ds.hub.ListReports()
238 _ = json.NewEncoder(w).Encode(reports)
239
240 case http.MethodPost:
241 body, err := io.ReadAll(io.LimitReader(r.Body, 8*1024*1024)) // 8MB ceiling for heap invariant
242 if err != nil {
243 http.Error(w, fmt.Sprintf("Failed to read payload: %v", err), http.StatusBadRequest)
244 return
245 }
246
247 var payload UploadReportPayload
248 if err := json.Unmarshal(body, &payload); err != nil {
249 http.Error(w, fmt.Sprintf("Malformed report JSON: %v", err), http.StatusBadRequest)
250 return
251 }
252
253 if payload.Report == nil {
254 http.Error(w, "Missing 'report' field in payload", http.StatusBadRequest)
255 return
256 }
257
258 clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
259 if payload.ClientIP == "" {
260 payload.ClientIP = clientIP
261 }
262
263 // Save report
264 reportID := ds.hub.StoreReport(
265 payload.Report,
266 []byte(payload.RawHTML),
267 []byte(payload.RawJSON),
268 []byte(payload.RawSARIF),
269 []byte(payload.Remediation),
270 payload.NetworkZone,
271 payload.ClientHost,
272 payload.ClientIP,
273 )
274
275 w.Header().Set("Content-Type", "application/json")
276 w.WriteHeader(http.StatusCreated)
277 _ = json.NewEncoder(w).Encode(map[string]string{
278 "status": "OK",
279 "report_id": reportID,
280 "url": fmt.Sprintf("/reports"),
281 })
282
283 default:
284 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
285 }
286}
287
288func (ds *DashboardServer) handleReportArtifacts(w http.ResponseWriter, r *http.Request) {
289 // Path pattern: /api/v1/reports/{id}/{artifact}
290 path := strings.TrimPrefix(r.URL.Path, "/api/v1/reports/")
291 parts := strings.Split(path, "/")
292 if len(parts) < 2 {
293 http.NotFound(w, r)
294 return
295 }
296
297 reportID := parts[0]
298 artifact := parts[1]
299
300 rep, ok := ds.hub.GetReport(reportID)
301 if !ok {
302 http.NotFound(w, r)
303 return
304 }
305
306 switch artifact {
307 case "html":
308 w.Header().Set("Content-Type", "text/html; charset=utf-8")
309 if len(rep.RawHTML) > 0 {
310 _, _ = w.Write(rep.RawHTML)
311 } else {
312 // Dynamically render if raw HTML wasn't provided
313 htmlStr, err := report.GenerateHTML(rep.Report)
314 if err != nil {
315 http.Error(w, fmt.Sprintf("Failed to render HTML: %v", err), http.StatusInternalServerError)
316 return
317 }
318 _, _ = w.Write([]byte(htmlStr))
319 }
320
321 case "json":
322 w.Header().Set("Content-Type", "application/json")
323 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"contraband_report_%s.json\"", reportID))
324 if len(rep.RawJSON) > 0 {
325 _, _ = w.Write(rep.RawJSON)
326 } else {
327 _ = json.NewEncoder(w).Encode(rep.Report)
328 }
329
330 case "sarif":
331 w.Header().Set("Content-Type", "application/json")
332 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"contraband_report_%s.sarif\"", reportID))
333 if len(rep.RawSARIF) > 0 {
334 _, _ = w.Write(rep.RawSARIF)
335 } else {
336 sarifBytes, err := report.ExportSARIF(rep.Report)
337 if err != nil {
338 http.Error(w, fmt.Sprintf("Failed to render SARIF: %v", err), http.StatusInternalServerError)
339 return
340 }
341 _, _ = w.Write(sarifBytes)
342 }
343
344 case "remediation":
345 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
346 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"contraband_remediation_%s.sh\"", reportID))
347 if len(rep.Remediation) > 0 {
348 _, _ = w.Write(rep.Remediation)
349 } else {
350 ruleSet := report.GenerateRemediation(rep.Report.Probes, rep.Report.TargetHost)
351 _, _ = w.Write([]byte(ruleSet.FormatAll()))
352 }
353
354 default:
355 http.NotFound(w, r)
356 }
357}
358
359func (ds *DashboardServer) handleReset(w http.ResponseWriter, r *http.Request) {
360 if r.Method != http.MethodPost {
361 http.Error(w, "Method not allowed, use POST", http.StatusMethodNotAllowed)
362 return
363 }
364
365 var req ResetRequest
366 if r.Body != nil {
367 _ = json.NewDecoder(r.Body).Decode(&req)
368 }
369
370 ds.mu.Lock()
371 cb := ds.resetCallback
372 evDir := ds.evidenceDir
373 ds.mu.Unlock()
374
375 var callbackErr error
376 if cb != nil {
377 callbackErr = cb(req.DeleteFiles)
378 }
379
380 ds.hub.Reset(req.DeleteFiles, evDir)
381
382 w.Header().Set("Content-Type", "application/json")
383 w.WriteHeader(http.StatusOK)
384 resp := ResetResponse{
385 Success: callbackErr == nil,
386 Message: "Dashboard telemetry reset successfully",
387 DeletedFiles: req.DeleteFiles,
388 EvidenceDir: evDir,
389 }
390 if callbackErr != nil {
391 resp.Message = fmt.Sprintf("Reset completed with warnings: %v", callbackErr)
392 }
393 _ = json.NewEncoder(w).Encode(resp)
394}