● 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 / report / serve.go
← Back to directory
266 lines · 7.6 KB
Raw
1package report
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "flag"
8 "fmt"
9 "net"
10 "net/http"
11 "os"
12 "os/exec"
13 "os/signal"
14 "runtime"
15 "strings"
16 "syscall"
17 "time"
18
19 "github.com/shehzade/contraband/pkg/common"
20)
21
22// ServerOptions configures the embedded reporting web server.
23type ServerOptions struct {
24 BindAddr string
25 InputPath string
26 Report *AssessmentReport
27 AutoOpen bool
28 ReadTimeout time.Duration
29 WriteTimeout time.Duration
30 ShutdownTimeout time.Duration
31}
32
33// MiniServer encapsulates the HTTP reporting listener and routes.
34type MiniServer struct {
35 httpServer *http.Server
36 listener net.Listener
37 report *AssessmentReport
38 addr string
39 autoOpen bool
40 startTime time.Time
41}
42
43// NewMiniServer initializes the HTTP server on the configured address.
44func NewMiniServer(opts ServerOptions) (*MiniServer, error) {
45 if opts.Report == nil {
46 if opts.InputPath == "" {
47 return nil, errors.New("must provide either Report or InputPath to MiniServer")
48 }
49 rep, err := LoadReport(opts.InputPath)
50 if err != nil {
51 return nil, fmt.Errorf("failed to load assessment report: %w", err)
52 }
53 opts.Report = rep
54 }
55
56 bind := opts.BindAddr
57 if bind == "" {
58 bind = "127.0.0.1:8088"
59 }
60 if !strings.Contains(bind, ":") {
61 bind = ":" + bind
62 }
63
64 ln, err := net.Listen("tcp", bind)
65 if err != nil {
66 return nil, fmt.Errorf("failed to bind report server on %s: %w", bind, err)
67 }
68
69 ms := &MiniServer{
70 listener: ln,
71 report: opts.Report,
72 addr: ln.Addr().String(),
73 autoOpen: opts.AutoOpen,
74 startTime: time.Now().UTC(),
75 }
76
77 mux := http.NewServeMux()
78 mux.HandleFunc("/", ms.handleRoot)
79 mux.HandleFunc("/api/summary", ms.handleSummary)
80 mux.HandleFunc("/api/timeline", ms.handleTimeline)
81 mux.HandleFunc("/api/remediation", ms.handleRemediation)
82 mux.HandleFunc("/healthz", ms.handleHealthz)
83
84 readTimeout := opts.ReadTimeout
85 if readTimeout == 0 {
86 readTimeout = 10 * time.Second
87 }
88 writeTimeout := opts.WriteTimeout
89 if writeTimeout == 0 {
90 writeTimeout = 30 * time.Second
91 }
92
93 ms.httpServer = &http.Server{
94 Handler: mux,
95 ReadTimeout: readTimeout,
96 WriteTimeout: writeTimeout,
97 }
98
99 return ms, nil
100}
101
102// URL returns the local accessible URL for the report server.
103func (s *MiniServer) URL() string {
104 host, port, err := net.SplitHostPort(s.addr)
105 if err != nil {
106 return fmt.Sprintf("http://%s", s.addr)
107 }
108 if host == "" || host == "0.0.0.0" || host == "::" {
109 host = "127.0.0.1"
110 }
111 return fmt.Sprintf("http://%s:%s", host, port)
112}
113
114// Start begins serving requests asynchronously.
115func (s *MiniServer) Start() error {
116 if s.autoOpen {
117 go func() {
118 time.Sleep(100 * time.Millisecond)
119 if err := OpenBrowser(s.URL()); err != nil {
120 common.GetLogger().Info(fmt.Sprintf("Browser auto-launch skipped (%v). Access report at: %s", err, s.URL()))
121 }
122 }()
123 }
124 return s.httpServer.Serve(s.listener)
125}
126
127// Shutdown gracefully terminates the HTTP server.
128func (s *MiniServer) Shutdown(ctx context.Context) error {
129 return s.httpServer.Shutdown(ctx)
130}
131
132// ServeReport starts the reporting mini-server, manages auto-open, and waits for interrupt signals.
133func ServeReport(ctx context.Context, opts ServerOptions) error {
134 server, err := NewMiniServer(opts)
135 if err != nil {
136 return err
137 }
138
139 logger := common.GetLogger()
140 logger.Info(fmt.Sprintf("Contraband Report Server listening at %s", server.URL()))
141 logger.Info("Press Ctrl+C to terminate the server")
142
143 errChan := make(chan error, 1)
144 go func() {
145 if err := server.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
146 errChan <- err
147 }
148 }()
149
150 sigChan := make(chan os.Signal, 1)
151 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
152
153 select {
154 case <-ctx.Done():
155 logger.Info("Context cancelled, shutting down report server...")
156 case sig := <-sigChan:
157 logger.Info(fmt.Sprintf("Received signal %v, shutting down report server...", sig))
158 case err := <-errChan:
159 return fmt.Errorf("server error: %w", err)
160 }
161
162 shutdownTimeout := opts.ShutdownTimeout
163 if shutdownTimeout == 0 {
164 shutdownTimeout = 5 * time.Second
165 }
166 shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
167 defer cancel()
168
169 return server.Shutdown(shutdownCtx)
170}
171
172// --- Handler Implementations ---
173
174func (s *MiniServer) handleRoot(w http.ResponseWriter, r *http.Request) {
175 if r.URL.Path != "/" {
176 http.NotFound(w, r)
177 return
178 }
179 w.Header().Set("Content-Type", "text/html; charset=utf-8")
180 w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
181 if err := WriteHTML(w, s.report); err != nil {
182 http.Error(w, fmt.Sprintf("Template rendering error: %v", err), http.StatusInternalServerError)
183 }
184}
185
186func (s *MiniServer) handleSummary(w http.ResponseWriter, r *http.Request) {
187 w.Header().Set("Content-Type", "application/json; charset=utf-8")
188 _ = json.NewEncoder(w).Encode(s.report.Summary)
189}
190
191func (s *MiniServer) handleTimeline(w http.ResponseWriter, r *http.Request) {
192 w.Header().Set("Content-Type", "application/json; charset=utf-8")
193 _ = json.NewEncoder(w).Encode(s.report.Probes)
194}
195
196func (s *MiniServer) handleRemediation(w http.ResponseWriter, r *http.Request) {
197 vendor := strings.ToLower(r.URL.Query().Get("vendor"))
198 if vendor != "" {
199 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
200 switch vendor {
201 case "pan_os", "pan", "paloalto":
202 _, _ = w.Write([]byte(s.report.Remediation.PANOSRules))
203 case "fortigate", "forti":
204 _, _ = w.Write([]byte(s.report.Remediation.FortiGateRules))
205 case "nftables", "nft":
206 _, _ = w.Write([]byte(s.report.Remediation.NFTablesRules))
207 case "iptables", "ipt":
208 _, _ = w.Write([]byte(s.report.Remediation.IPTablesRules))
209 case "cisco_asa", "asa", "cisco":
210 _, _ = w.Write([]byte(s.report.Remediation.CiscoASARules))
211 default:
212 http.Error(w, fmt.Sprintf("Unknown vendor '%s'", vendor), http.StatusBadRequest)
213 }
214 return
215 }
216
217 if strings.Contains(r.Header.Get("Accept"), "text/plain") {
218 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
219 combined := fmt.Sprintf("# === PALO ALTO NETWORKS ===\n%s\n\n# === FORTINET FORTIGATE ===\n%s\n\n# === LINUX NFTABLES ===\n%s\n\n# === LINUX IPTABLES ===\n%s\n\n# === CISCO ASA ===\n%s\n",
220 s.report.Remediation.PANOSRules, s.report.Remediation.FortiGateRules, s.report.Remediation.NFTablesRules, s.report.Remediation.IPTablesRules, s.report.Remediation.CiscoASARules)
221 _, _ = w.Write([]byte(combined))
222 return
223 }
224
225 w.Header().Set("Content-Type", "application/json; charset=utf-8")
226 _ = json.NewEncoder(w).Encode(s.report.Remediation)
227}
228
229func (s *MiniServer) handleHealthz(w http.ResponseWriter, r *http.Request) {
230 w.Header().Set("Content-Type", "application/json; charset=utf-8")
231 uptime := time.Since(s.startTime)
232 _ = json.NewEncoder(w).Encode(map[string]interface{}{
233 "status": "ok",
234 "uptime_ms": float64(uptime.Microseconds()) / 1000.0,
235 })
236}
237
238// OpenBrowser launches the client's default desktop browser with headless fallback.
239func OpenBrowser(url string) error {
240 // Suppress browser spawning when explicitly disabled or during automated testing
241 if os.Getenv("CONTRABAND_NO_BROWSER") != "" {
242 return nil
243 }
244
245 switch runtime.GOOS {
246 case "windows":
247 if flag.Lookup("test.v") != nil {
248 return nil
249 }
250 return exec.Command("cmd", "/c", "start", url).Start()
251 case "darwin":
252 if flag.Lookup("test.v") != nil {
253 return nil
254 }
255 return exec.Command("open", url).Start()
256 default: // Linux, FreeBSD, OpenBSD
257 // Check for headless environment
258 if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
259 return errors.New("headless environment detected: no DISPLAY or WAYLAND_DISPLAY available")
260 }
261 if flag.Lookup("test.v") != nil {
262 return nil
263 }
264 return exec.Command("xdg-open", url).Start()
265 }
266}