● 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 / soak / soak.go
← Back to directory
463 lines · 13.8 KB
Raw
1package soak
2
3import (
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.
26type 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.
42func 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.
59type 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.
78type 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.
97func 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.
159func (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.
167func (e *SoakEngine) MemoryMonitor() *MemoryMonitor {
168 return e.memMonitor
169}
170
171// Matrix returns the full scenario matrix.
172func (e *SoakEngine) Matrix() []Scenario {
173 return e.matrix
174}
175
176// ExecuteScenario executes a single matrix scenario (live exfil or negative test injection).
177func (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
337func (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.
348func (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.
432func (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}