[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)
463 lines · 13.8 KB
| 1 | package soak |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | "net/http" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "runtime" |
| 12 | "runtime/debug" |
| 13 | "sync" |
| 14 | "sync/atomic" |
| 15 | "time" |
| 16 | |
| 17 | "github.com/shehzade/contraband/pkg/client" |
| 18 | "github.com/shehzade/contraband/pkg/client/dlp" |
| 19 | "github.com/shehzade/contraband/pkg/client/exfil" |
| 20 | "github.com/shehzade/contraband/pkg/common" |
| 21 | "github.com/shehzade/contraband/pkg/report" |
| 22 | "github.com/shehzade/contraband/pkg/server/live" |
| 23 | ) |
| 24 | |
| 25 | // SoakConfig configures the overnight continuous soak and stress engine. |
| 26 | type SoakConfig struct { |
| 27 | TargetServer string `json:"target_server"` |
| 28 | DashboardURL string `json:"dashboard_url"` |
| 29 | Duration time.Duration `json:"duration"` |
| 30 | LogPath string `json:"log_path"` |
| 31 | MaxHeapCeiling uint64 `json:"max_heap_ceiling"` |
| 32 | SampleInterval time.Duration `json:"sample_interval"` |
| 33 | ReportInterval time.Duration `json:"report_interval"` |
| 34 | Volumes []int64 `json:"volumes"` |
| 35 | ValidationVolumes []int64 `json:"validation_volumes"` |
| 36 | Concurrency int `json:"concurrency"` |
| 37 | DryRun bool `json:"dry_run"` |
| 38 | EvidenceDir string `json:"evidence_dir"` |
| 39 | } |
| 40 | |
| 41 | // DefaultSoakConfig returns production defaults for the 8-10 hour continuous soak engine. |
| 42 | func DefaultSoakConfig() SoakConfig { |
| 43 | return SoakConfig{ |
| 44 | TargetServer: "127.0.0.1", |
| 45 | DashboardURL: "http://127.0.0.1:8080", |
| 46 | Duration: 10 * time.Hour, |
| 47 | LogPath: "evidence/soak_test.log", |
| 48 | MaxHeapCeiling: DefaultMaxHeapCeiling, // 32MB |
| 49 | SampleInterval: 500 * time.Millisecond, |
| 50 | ReportInterval: 2 * time.Minute, |
| 51 | Volumes: AllVolumeTiers(), |
| 52 | ValidationVolumes: ValidationVolumeTiers(), |
| 53 | Concurrency: len(AllZones()), // 5 concurrent zones |
| 54 | EvidenceDir: "evidence", |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | // SoakSummary provides high-level summary statistics of the soak execution. |
| 59 | type SoakSummary struct { |
| 60 | StartTime time.Time `json:"start_time"` |
| 61 | ElapsedTime time.Duration `json:"elapsed_time"` |
| 62 | ElapsedMS float64 `json:"elapsed_ms"` |
| 63 | TotalScenarios int64 `json:"total_scenarios"` |
| 64 | SuccessScenarios int64 `json:"success_scenarios"` |
| 65 | FailedScenarios int64 `json:"failed_scenarios"` |
| 66 | TotalBytesSmuggled int64 `json:"total_bytes_smuggled"` |
| 67 | FormattedBytes string `json:"formatted_bytes"` |
| 68 | ReportsUploaded int64 `json:"reports_uploaded"` |
| 69 | NegativePasses int64 `json:"negative_passes"` |
| 70 | NegativeFails int64 `json:"negative_fails"` |
| 71 | PeakHeapMB float64 `json:"peak_heap_mb"` |
| 72 | MemoryViolations int64 `json:"memory_violations"` |
| 73 | MemoryHealthy bool `json:"memory_healthy"` |
| 74 | } |
| 75 | |
| 76 | // SoakEngine orchestrates multi-zone concurrent streaming, memory bounding, |
| 77 | // continuous evidence logging, negative disruption injection, and live dashboard synchronization. |
| 78 | type SoakEngine struct { |
| 79 | mu sync.RWMutex |
| 80 | config SoakConfig |
| 81 | logger *SoakLogger |
| 82 | memMonitor *MemoryMonitor |
| 83 | matrix []Scenario |
| 84 | probesTotal int64 |
| 85 | probesSuccess int64 |
| 86 | probesFailed int64 |
| 87 | bytesTransferred int64 |
| 88 | reportsUploaded int64 |
| 89 | negativePasses int64 |
| 90 | negativeFails int64 |
| 91 | startTime time.Time |
| 92 | recentProbesMu sync.Mutex |
| 93 | recentProbes []common.ProbeResult |
| 94 | } |
| 95 | |
| 96 | // NewSoakEngine instantiates and initializes all soak components. |
| 97 | func NewSoakEngine(cfg SoakConfig) (*SoakEngine, error) { |
| 98 | if cfg.TargetServer == "" { |
| 99 | cfg.TargetServer = "127.0.0.1" |
| 100 | } |
| 101 | if cfg.DashboardURL == "" { |
| 102 | cfg.DashboardURL = "http://127.0.0.1:8080" |
| 103 | } |
| 104 | if cfg.LogPath == "" { |
| 105 | cfg.LogPath = "evidence/soak_test.log" |
| 106 | } |
| 107 | if cfg.MaxHeapCeiling == 0 { |
| 108 | cfg.MaxHeapCeiling = DefaultMaxHeapCeiling |
| 109 | } |
| 110 | if cfg.SampleInterval <= 0 { |
| 111 | cfg.SampleInterval = 500 * time.Millisecond |
| 112 | } |
| 113 | if cfg.ReportInterval <= 0 { |
| 114 | cfg.ReportInterval = 2 * time.Minute |
| 115 | } |
| 116 | if len(cfg.Volumes) == 0 { |
| 117 | cfg.Volumes = AllVolumeTiers() |
| 118 | } |
| 119 | if len(cfg.ValidationVolumes) == 0 { |
| 120 | cfg.ValidationVolumes = ValidationVolumeTiers() |
| 121 | } |
| 122 | if cfg.Concurrency <= 0 { |
| 123 | cfg.Concurrency = len(AllZones()) |
| 124 | } |
| 125 | if cfg.EvidenceDir == "" { |
| 126 | cfg.EvidenceDir = "evidence" |
| 127 | } |
| 128 | |
| 129 | logger, err := NewSoakLogger(cfg.LogPath) |
| 130 | if err != nil { |
| 131 | return nil, fmt.Errorf("failed to initialize soak logger: %w", err) |
| 132 | } |
| 133 | |
| 134 | // Strictly bound memory heap ceiling via runtime directives |
| 135 | debug.SetGCPercent(20) |
| 136 | debug.SetMemoryLimit(int64(cfg.MaxHeapCeiling)) |
| 137 | |
| 138 | memMonitor := NewMemoryMonitor(cfg.MaxHeapCeiling, cfg.SampleInterval) |
| 139 | matrix := GenerateScenarioMatrix(cfg.Volumes) |
| 140 | |
| 141 | engine := &SoakEngine{ |
| 142 | config: cfg, |
| 143 | logger: logger, |
| 144 | memMonitor: memMonitor, |
| 145 | matrix: matrix, |
| 146 | recentProbes: make([]common.ProbeResult, 0, 100), |
| 147 | } |
| 148 | |
| 149 | // Register memory violation callback to log alerts immediately |
| 150 | memMonitor.SetOnViolation(func(stats MemoryStats) { |
| 151 | logger.LogAlert(fmt.Sprintf("MEMORY INVARIANT BREACHED: HeapAlloc=%.2fMB exceeds ceiling=%.2fMB", |
| 152 | stats.HeapAllocMB, stats.CeilingMB)) |
| 153 | }) |
| 154 | |
| 155 | return engine, nil |
| 156 | } |
| 157 | |
| 158 | // Close gracefully closes loggers and background resources. |
| 159 | func (e *SoakEngine) Close() error { |
| 160 | if e.logger != nil { |
| 161 | return e.logger.Close() |
| 162 | } |
| 163 | return nil |
| 164 | } |
| 165 | |
| 166 | // MemoryMonitor returns the active MemoryMonitor instance. |
| 167 | func (e *SoakEngine) MemoryMonitor() *MemoryMonitor { |
| 168 | return e.memMonitor |
| 169 | } |
| 170 | |
| 171 | // Matrix returns the full scenario matrix. |
| 172 | func (e *SoakEngine) Matrix() []Scenario { |
| 173 | return e.matrix |
| 174 | } |
| 175 | |
| 176 | // ExecuteScenario executes a single matrix scenario (live exfil or negative test injection). |
| 177 | func (e *SoakEngine) ExecuteScenario(ctx context.Context, sc Scenario) error { |
| 178 | atomic.AddInt64(&e.probesTotal, 1) |
| 179 | |
| 180 | // Case 1: Negative Testing Disruption Injection |
| 181 | if sc.IsNegative { |
| 182 | port := client.DefaultPortForProtocol(sc.Protocol) |
| 183 | negRes := ExecuteNegativeScenario(ctx, sc, e.config.TargetServer, port) |
| 184 | |
| 185 | if negRes.Passed { |
| 186 | atomic.AddInt64(&e.negativePasses, 1) |
| 187 | atomic.AddInt64(&e.probesSuccess, 1) |
| 188 | } else { |
| 189 | atomic.AddInt64(&e.negativeFails, 1) |
| 190 | atomic.AddInt64(&e.probesFailed, 1) |
| 191 | } |
| 192 | |
| 193 | e.logger.LogNegativeTest( |
| 194 | sc.NegativeType, |
| 195 | sc.Zone, |
| 196 | negRes.Target, |
| 197 | negRes.ActualVerdict, |
| 198 | negRes.ExpectedVerdict, |
| 199 | negRes.LatencyMS, |
| 200 | negRes.Passed, |
| 201 | ) |
| 202 | |
| 203 | probe := common.ProbeResult{ |
| 204 | ID: fmt.Sprintf("soak-neg-%d", time.Now().UnixNano()), |
| 205 | Timestamp: time.Now().UTC(), |
| 206 | Stage: "soak_negative", |
| 207 | Protocol: sc.Protocol, |
| 208 | TargetHost: e.config.TargetServer, |
| 209 | TargetPort: port, |
| 210 | PayloadType: sc.DLPType, |
| 211 | CarrierType: sc.CarrierType, |
| 212 | Verdict: negRes.ActualVerdict, |
| 213 | ErrorMessage: negRes.ErrorMessage, |
| 214 | } |
| 215 | probe.Timestamps.TotalDuration = time.Duration(negRes.LatencyMS * float64(time.Millisecond)) |
| 216 | e.recordRecentProbe(probe) |
| 217 | return nil |
| 218 | } |
| 219 | |
| 220 | // Case 2: Live Protocol Exfiltration Stream |
| 221 | ex, err := exfil.GetExfiltrator(sc.Protocol) |
| 222 | if err != nil { |
| 223 | atomic.AddInt64(&e.probesFailed, 1) |
| 224 | e.logger.LogAlert(fmt.Sprintf("protocol %q not registered: %v", sc.Protocol, err)) |
| 225 | return err |
| 226 | } |
| 227 | |
| 228 | port := client.DefaultPortForProtocol(sc.Protocol) |
| 229 | isTLS := port == 443 || port == 8443 || port == 9443 || port == 10443 || port == 853 || port == 990 || port == 465 || port == 4430 |
| 230 | sessionToken, _ := common.NewSessionToken() |
| 231 | |
| 232 | chunkSize := common.DefaultChunkSize |
| 233 | if IsCovertProtocol(sc.Protocol) { |
| 234 | chunkSize = 512 |
| 235 | } |
| 236 | timeout := 45 * time.Second |
| 237 | if sc.VolumeBytes > Volume100MB { |
| 238 | timeout = time.Duration(sc.VolumeBytes/(2*1024*1024))*time.Second + 30*time.Second |
| 239 | } |
| 240 | |
| 241 | exfilOpts := exfil.ExfilOptions{ |
| 242 | TargetHost: e.config.TargetServer, |
| 243 | TargetPort: port, |
| 244 | UseTLS: isTLS, |
| 245 | InsecureTLS: true, |
| 246 | TLSFingerprint: sc.TLSFingerprint, |
| 247 | SessionToken: sessionToken, |
| 248 | ChunkSize: chunkSize, |
| 249 | TotalBytes: sc.VolumeBytes, |
| 250 | Timeout: timeout, |
| 251 | PayloadType: sc.DLPType, |
| 252 | CarrierType: sc.CarrierType, |
| 253 | Headers: make(map[string]string), |
| 254 | } |
| 255 | exfilOpts.Headers["X-Contraband-Zone"] = sc.Zone |
| 256 | |
| 257 | dlpConfig := dlp.GeneratorConfig{ |
| 258 | DLPType: sc.DLPType, |
| 259 | CarrierType: sc.CarrierType, |
| 260 | Seed: time.Now().UnixNano(), |
| 261 | } |
| 262 | |
| 263 | streamer := dlp.NewDLPStreamer(sc.VolumeBytes, dlpConfig) |
| 264 | defer streamer.Close() |
| 265 | |
| 266 | // Leave ExpectedHash empty so streaming exfiltration does not assert against pre-read zero hash. |
| 267 | exfilOpts.ExpectedHash = "" |
| 268 | |
| 269 | if sc.Transforms != "" { |
| 270 | exfilOpts.Headers["X-Contraband-Transforms"] = sc.Transforms |
| 271 | } |
| 272 | if sc.Stego != "" { |
| 273 | exfilOpts.Headers["X-Contraband-Stego"] = sc.Stego |
| 274 | } |
| 275 | if sc.Cadence != "" { |
| 276 | exfilOpts.Headers["X-Contraband-Cadence"] = sc.Cadence |
| 277 | } |
| 278 | |
| 279 | start := time.Now() |
| 280 | res, exErr := ex.Exfiltrate(ctx, streamer, exfilOpts) |
| 281 | dur := time.Since(start) |
| 282 | latMs := float64(dur.Microseconds()) / 1000.0 |
| 283 | |
| 284 | // Immediate proactive garbage collection and memory sampling |
| 285 | runtime.GC() |
| 286 | memStats := e.memMonitor.Sample() |
| 287 | |
| 288 | expectedHash := streamer.ChecksumHex() |
| 289 | |
| 290 | verdict := common.VerdictSuccess |
| 291 | var errMsg string |
| 292 | if exErr != nil { |
| 293 | verdict = common.ClassifyError(exErr) |
| 294 | errMsg = exErr.Error() |
| 295 | atomic.AddInt64(&e.probesFailed, 1) |
| 296 | } else { |
| 297 | atomic.AddInt64(&e.probesSuccess, 1) |
| 298 | atomic.AddInt64(&e.bytesTransferred, sc.VolumeBytes) |
| 299 | } |
| 300 | |
| 301 | e.logger.LogProbe( |
| 302 | sc.Zone, |
| 303 | sc.Protocol, |
| 304 | sc.VolumeStr, |
| 305 | verdict, |
| 306 | latMs, |
| 307 | memStats.HeapAllocMB, |
| 308 | memStats.CeilingMB, |
| 309 | expectedHash, |
| 310 | errMsg, |
| 311 | ) |
| 312 | |
| 313 | var probe common.ProbeResult |
| 314 | if res != nil { |
| 315 | probe = *res |
| 316 | } else { |
| 317 | probe = common.ProbeResult{ |
| 318 | ID: fmt.Sprintf("soak-%d", time.Now().UnixNano()), |
| 319 | Timestamp: time.Now().UTC(), |
| 320 | Stage: "soak_exfil", |
| 321 | Protocol: sc.Protocol, |
| 322 | TargetHost: e.config.TargetServer, |
| 323 | TargetPort: port, |
| 324 | PayloadType: sc.DLPType, |
| 325 | CarrierType: sc.CarrierType, |
| 326 | PayloadHash: expectedHash, |
| 327 | Verdict: verdict, |
| 328 | ErrorMessage: errMsg, |
| 329 | } |
| 330 | } |
| 331 | probe.Timestamps.TotalDuration = dur |
| 332 | e.recordRecentProbe(probe) |
| 333 | |
| 334 | return nil |
| 335 | } |
| 336 | |
| 337 | func (e *SoakEngine) recordRecentProbe(p common.ProbeResult) { |
| 338 | e.recentProbesMu.Lock() |
| 339 | defer e.recentProbesMu.Unlock() |
| 340 | if len(e.recentProbes) >= 100 { |
| 341 | e.recentProbes = e.recentProbes[1:] |
| 342 | } |
| 343 | e.recentProbes = append(e.recentProbes, p) |
| 344 | } |
| 345 | |
| 346 | // GenerateAndSyncReport compiles the accumulated probes into an AssessmentReport, |
| 347 | // generates HTML/JSON/SARIF artifacts, and uploads them to the live dashboard report server. |
| 348 | func (e *SoakEngine) GenerateAndSyncReport(ctx context.Context, zone string) error { |
| 349 | e.recentProbesMu.Lock() |
| 350 | probesCopy := make([]common.ProbeResult, len(e.recentProbes)) |
| 351 | copy(probesCopy, e.recentProbes) |
| 352 | e.recentProbesMu.Unlock() |
| 353 | |
| 354 | if len(probesCopy) == 0 { |
| 355 | return nil |
| 356 | } |
| 357 | |
| 358 | start := time.Now() |
| 359 | rep := report.NewAssessmentReport(e.config.TargetServer, probesCopy, nil, nil, 10*time.Second) |
| 360 | |
| 361 | rawJSON, _ := report.ExportJSON(rep) |
| 362 | htmlStr, _ := report.GenerateHTML(rep) |
| 363 | rawHTML := []byte(htmlStr) |
| 364 | rawSARIF, _ := report.ExportSARIF(rep) |
| 365 | |
| 366 | remediationSet := report.GenerateRemediation(probesCopy, e.config.TargetServer) |
| 367 | var remediationScript []byte |
| 368 | if remediationSet != nil { |
| 369 | remediationScript = []byte(remediationSet.FormatAll()) |
| 370 | } |
| 371 | |
| 372 | durMs := float64(time.Since(start).Microseconds()) / 1000.0 |
| 373 | |
| 374 | // Upload to Dashboard |
| 375 | if e.config.DashboardURL != "" { |
| 376 | baseURL := e.config.DashboardURL |
| 377 | targetURL := baseURL + "/api/v1/reports" |
| 378 | |
| 379 | hostname, _ := os.Hostname() |
| 380 | if hostname == "" { |
| 381 | hostname = "contraband-soak-worker" |
| 382 | } |
| 383 | if zone == "" { |
| 384 | zone = ZoneLANCorp |
| 385 | } |
| 386 | |
| 387 | payload := live.UploadReportPayload{ |
| 388 | Report: rep, |
| 389 | RawHTML: string(rawHTML), |
| 390 | RawJSON: string(rawJSON), |
| 391 | RawSARIF: string(rawSARIF), |
| 392 | Remediation: string(remediationScript), |
| 393 | NetworkZone: zone, |
| 394 | ClientHost: hostname, |
| 395 | ClientIP: "127.0.0.1", |
| 396 | } |
| 397 | |
| 398 | payloadBytes, mErr := json.Marshal(payload) |
| 399 | if mErr == nil { |
| 400 | req, rErr := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(payloadBytes)) |
| 401 | if rErr == nil { |
| 402 | req.Header.Set("Content-Type", "application/json") |
| 403 | client := &http.Client{Timeout: 5 * time.Second} |
| 404 | resp, pErr := client.Do(req) |
| 405 | if pErr == nil { |
| 406 | _ = resp.Body.Close() |
| 407 | if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK { |
| 408 | atomic.AddInt64(&e.reportsUploaded, 1) |
| 409 | e.logger.LogReportUploaded(rep.ID, zone, rep.Summary.EEIScore, string(rep.Summary.Posture), durMs) |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | // Also persist local forensic report snapshots into evidence dir |
| 417 | if e.config.EvidenceDir != "" { |
| 418 | _ = os.MkdirAll(e.config.EvidenceDir, 0755) |
| 419 | jsonPath := filepath.Join(e.config.EvidenceDir, fmt.Sprintf("soak_report_%s.json", zone)) |
| 420 | htmlPath := filepath.Join(e.config.EvidenceDir, fmt.Sprintf("soak_report_%s.html", zone)) |
| 421 | sarifPath := filepath.Join(e.config.EvidenceDir, fmt.Sprintf("soak_report_%s.sarif", zone)) |
| 422 | |
| 423 | _ = os.WriteFile(jsonPath, rawJSON, 0644) |
| 424 | _ = os.WriteFile(htmlPath, rawHTML, 0644) |
| 425 | _ = os.WriteFile(sarifPath, rawSARIF, 0644) |
| 426 | } |
| 427 | |
| 428 | return nil |
| 429 | } |
| 430 | |
| 431 | // Summary returns the current execution metrics summary. |
| 432 | func (e *SoakEngine) Summary() SoakSummary { |
| 433 | elapsed := time.Since(e.startTime) |
| 434 | elapsedMs := float64(elapsed.Microseconds()) / 1000.0 |
| 435 | |
| 436 | tot := atomic.LoadInt64(&e.probesTotal) |
| 437 | succ := atomic.LoadInt64(&e.probesSuccess) |
| 438 | fail := atomic.LoadInt64(&e.probesFailed) |
| 439 | bytesXfer := atomic.LoadInt64(&e.bytesTransferred) |
| 440 | reps := atomic.LoadInt64(&e.reportsUploaded) |
| 441 | negPass := atomic.LoadInt64(&e.negativePasses) |
| 442 | negFail := atomic.LoadInt64(&e.negativeFails) |
| 443 | |
| 444 | peakMB := e.memMonitor.PeakHeapAllocMB() |
| 445 | violations := e.memMonitor.Violations() |
| 446 | |
| 447 | return SoakSummary{ |
| 448 | StartTime: e.startTime, |
| 449 | ElapsedTime: elapsed, |
| 450 | ElapsedMS: elapsedMs, |
| 451 | TotalScenarios: tot, |
| 452 | SuccessScenarios: succ, |
| 453 | FailedScenarios: fail, |
| 454 | TotalBytesSmuggled: bytesXfer, |
| 455 | FormattedBytes: common.FormatBytes(bytesXfer), |
| 456 | ReportsUploaded: reps, |
| 457 | NegativePasses: negPass, |
| 458 | NegativeFails: negFail, |
| 459 | PeakHeapMB: peakMB, |
| 460 | MemoryViolations: violations, |
| 461 | MemoryHealthy: violations == 0 && peakMB < float64(e.config.MaxHeapCeiling)/(1024*1024), |
| 462 | } |
| 463 | } |