[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)
458 lines · 15.0 KB
| 1 | package common |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "net/url" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | "time" |
| 12 | |
| 13 | "gopkg.in/yaml.v3" |
| 14 | ) |
| 15 | |
| 16 | // ProfileConfig encapsulates full assessment configurations for the Contraband client engine. |
| 17 | type ProfileConfig struct { |
| 18 | Target TargetConfig `yaml:"target" json:"target"` |
| 19 | DLP DLPConfig `yaml:"dlp" json:"dlp"` |
| 20 | Evasion EvasionConfig `yaml:"evasion" json:"evasion"` |
| 21 | Traffic TrafficConfig `yaml:"traffic" json:"traffic"` |
| 22 | Proxy ProxyConfig `yaml:"proxy" json:"proxy"` |
| 23 | Sweep SweepConfig `yaml:"sweep" json:"sweep"` |
| 24 | Output OutputConfig `yaml:"output" json:"output"` |
| 25 | Destroy bool `yaml:"destroy" json:"destroy"` |
| 26 | } |
| 27 | |
| 28 | type TargetConfig struct { |
| 29 | Server string `yaml:"server" json:"server"` |
| 30 | Ports string `yaml:"ports" json:"ports"` // 'quick', 'standard', 'full', or comma/range |
| 31 | Protocols []string `yaml:"protocols" json:"protocols"` // 'all', 'web', 'dns', etc. |
| 32 | } |
| 33 | |
| 34 | type DLPConfig struct { |
| 35 | Payloads []string `yaml:"payloads" json:"payloads"` // 'all', 'pci', 'pii', 'hipaa', 'secrets' |
| 36 | CarrierType string `yaml:"carrier_type" json:"carrier_type"` // 'csv', 'json', 'sql', 'email', 'syslog', 'noise' |
| 37 | Size string `yaml:"size" json:"size"` // '100KB', '50MB', '2GB' |
| 38 | ChunkSize string `yaml:"chunk_size" json:"chunk_size"` // '64KB', '1MB', '10MB' |
| 39 | } |
| 40 | |
| 41 | type EvasionConfig struct { |
| 42 | Transforms []string `yaml:"transforms" json:"transforms"` // 'raw', 'base64', 'hex', 'gzip', 'zstd', 'aes_gcm' |
| 43 | Stego string `yaml:"stego" json:"stego"` // 'none', 'png', 'jpeg', 'pdf', 'zip' |
| 44 | TLSFingerprint string `yaml:"tls_fingerprint" json:"tls_fingerprint"` // 'chrome', 'edge', 'safari', 'firefox', 'curl', 'default' |
| 45 | } |
| 46 | |
| 47 | type TrafficConfig struct { |
| 48 | Threads int `yaml:"threads" json:"threads"` // 1–64 |
| 49 | Throughput string `yaml:"throughput" json:"throughput"` // '100KB/s', '5MB/s', 'unlimited' |
| 50 | Cadence string `yaml:"cadence" json:"cadence"` // 'burst', 'steady', 'jittered', 'office-hours', 'low-and-slow' |
| 51 | IntervalMin string `yaml:"interval_min" json:"interval_min"` // e.g. '500ms' |
| 52 | IntervalMax string `yaml:"interval_max" json:"interval_max"` // e.g. '4s' |
| 53 | Jitter float64 `yaml:"jitter,omitempty" json:"jitter,omitempty"` // 0.0–1.0 (ratio) or 0–100 (percentage) |
| 54 | } |
| 55 | |
| 56 | type ProxyConfig struct { |
| 57 | Mode string `yaml:"mode" json:"mode"` // 'auto', 'direct', or URL |
| 58 | AuthType string `yaml:"auth_type" json:"auth_type"` // 'basic', 'ntlm', 'negotiate' |
| 59 | } |
| 60 | |
| 61 | type SweepConfig struct { |
| 62 | Concurrency int `yaml:"concurrency" json:"concurrency"` // 1–1000 |
| 63 | RateLimit int `yaml:"rate_limit" json:"rate_limit"` // packets/sec |
| 64 | } |
| 65 | |
| 66 | type OutputConfig struct { |
| 67 | JSONPath string `yaml:"json_path" json:"json_path"` |
| 68 | HTMLPath string `yaml:"html_path" json:"html_path"` |
| 69 | } |
| 70 | |
| 71 | // NewDefaultProfileConfig initializes a ProfileConfig with safe, production-grade defaults. |
| 72 | func NewDefaultProfileConfig() *ProfileConfig { |
| 73 | return &ProfileConfig{ |
| 74 | Target: TargetConfig{ |
| 75 | Server: "127.0.0.1", |
| 76 | Ports: PortPresetQuick, |
| 77 | Protocols: []string{CategoryAll}, |
| 78 | }, |
| 79 | DLP: DLPConfig{ |
| 80 | Payloads: []string{DLPTypeAll}, |
| 81 | CarrierType: CarrierCSV, |
| 82 | Size: "1MB", |
| 83 | ChunkSize: "1MB", |
| 84 | }, |
| 85 | Evasion: EvasionConfig{ |
| 86 | Transforms: []string{TransformRaw}, |
| 87 | Stego: StegoNone, |
| 88 | TLSFingerprint: TLSFingerprintChrome, |
| 89 | }, |
| 90 | Traffic: TrafficConfig{ |
| 91 | Threads: DefaultThreads, |
| 92 | Throughput: "unlimited", |
| 93 | Cadence: CadenceSteady, |
| 94 | IntervalMin: "500ms", |
| 95 | IntervalMax: "4s", |
| 96 | }, |
| 97 | Proxy: ProxyConfig{ |
| 98 | Mode: "auto", |
| 99 | AuthType: "basic", |
| 100 | }, |
| 101 | Sweep: SweepConfig{ |
| 102 | Concurrency: DefaultConcurrency, |
| 103 | RateLimit: DefaultRateLimit, |
| 104 | }, |
| 105 | Output: OutputConfig{ |
| 106 | JSONPath: "results.json", |
| 107 | HTMLPath: "report.html", |
| 108 | }, |
| 109 | Destroy: false, |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // NewStealthProfileConfig creates a low-and-slow, obfuscated profile. |
| 114 | func NewStealthProfileConfig() *ProfileConfig { |
| 115 | cfg := NewDefaultProfileConfig() |
| 116 | cfg.Traffic.Threads = 1 |
| 117 | cfg.Traffic.Throughput = "50KB/s" |
| 118 | cfg.Traffic.Cadence = CadenceLowAndSlow |
| 119 | cfg.Traffic.IntervalMin = "2s" |
| 120 | cfg.Traffic.IntervalMax = "10s" |
| 121 | cfg.Evasion.Transforms = []string{TransformGzip, TransformAESGCM} |
| 122 | cfg.Evasion.Stego = StegoPNG |
| 123 | cfg.Evasion.TLSFingerprint = TLSFingerprintChrome |
| 124 | cfg.Sweep.Concurrency = 10 |
| 125 | cfg.Sweep.RateLimit = 20 |
| 126 | return cfg |
| 127 | } |
| 128 | |
| 129 | // NewAggressiveProfileConfig creates a high-throughput, maximum-concurrency profile. |
| 130 | func NewAggressiveProfileConfig() *ProfileConfig { |
| 131 | cfg := NewDefaultProfileConfig() |
| 132 | cfg.Target.Ports = PortPresetStandard |
| 133 | cfg.Traffic.Threads = 32 |
| 134 | cfg.Traffic.Throughput = "unlimited" |
| 135 | cfg.Traffic.Cadence = CadenceBurst |
| 136 | cfg.Sweep.Concurrency = 500 |
| 137 | cfg.Sweep.RateLimit = 1000 |
| 138 | return cfg |
| 139 | } |
| 140 | |
| 141 | // NewComplianceProfileConfig creates a multi-DLP comprehensive audit profile. |
| 142 | func NewComplianceProfileConfig() *ProfileConfig { |
| 143 | cfg := NewDefaultProfileConfig() |
| 144 | cfg.DLP.Payloads = []string{DLPTypePCI, DLPTypePII, DLPTypeHIPAA, DLPTypeSecrets} |
| 145 | cfg.DLP.Size = "10MB" |
| 146 | cfg.DLP.CarrierType = CarrierJSON |
| 147 | return cfg |
| 148 | } |
| 149 | |
| 150 | // LoadProfileConfig reads, deserializes, and validates a YAML or JSON profile file. |
| 151 | func LoadProfileConfig(path string) (*ProfileConfig, error) { |
| 152 | data, err := os.ReadFile(path) |
| 153 | if err != nil { |
| 154 | return nil, fmt.Errorf("failed to read profile config file: %w", err) |
| 155 | } |
| 156 | |
| 157 | cfg := NewDefaultProfileConfig() |
| 158 | ext := strings.ToLower(filepath.Ext(path)) |
| 159 | |
| 160 | if ext == ".json" { |
| 161 | if err := json.Unmarshal(data, cfg); err != nil { |
| 162 | return nil, fmt.Errorf("failed to parse JSON profile config: %w", err) |
| 163 | } |
| 164 | } else { |
| 165 | if err := yaml.Unmarshal(data, cfg); err != nil { |
| 166 | return nil, fmt.Errorf("failed to parse YAML profile config: %w", err) |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | if err := cfg.Validate(); err != nil { |
| 171 | return nil, fmt.Errorf("profile config validation error: %w", err) |
| 172 | } |
| 173 | |
| 174 | return cfg, nil |
| 175 | } |
| 176 | |
| 177 | // Validate executes strict semantic validation against all profile configuration fields. |
| 178 | func (c *ProfileConfig) Validate() error { |
| 179 | // Validate Ports |
| 180 | if err := validatePortsField(c.Target.Ports); err != nil { |
| 181 | return fmt.Errorf("invalid target ports: %w", err) |
| 182 | } |
| 183 | |
| 184 | // Validate Protocols |
| 185 | if len(c.Target.Protocols) == 0 { |
| 186 | return errors.New("at least one protocol must be specified") |
| 187 | } |
| 188 | validProtocols := map[string]bool{ |
| 189 | "all": true, "web": true, "dns": true, "file": true, "mail": true, "smb": true, "cloud": true, "covert": true, |
| 190 | ProtoHTTP1: true, ProtoHTTP2: true, ProtoHTTP3: true, ProtoWebSocket: true, ProtoGRPC: true, ProtoWebDAV: true, |
| 191 | ProtoDNSA: true, ProtoDNSTXT: true, ProtoDoH: true, ProtoDoT: true, |
| 192 | ProtoFTP: true, ProtoFTPS: true, ProtoSFTP: true, ProtoSMTP: true, |
| 193 | ProtoSMBFile: true, ProtoSMBHash: true, ProtoSMBQUIC: true, |
| 194 | ProtoAWSS3: true, ProtoGCPGCS: true, ProtoAzureBlob: true, |
| 195 | ProtoICMPEcho: true, ProtoNTPTime: true, ProtoSyslog: true, ProtoMQTT: true, |
| 196 | } |
| 197 | for _, p := range c.Target.Protocols { |
| 198 | if !validProtocols[strings.ToLower(p)] { |
| 199 | return fmt.Errorf("unsupported protocol or category: '%s'", p) |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | // Validate DLP Payloads |
| 204 | validDLP := map[string]bool{"all": true, DLPTypePCI: true, DLPTypePII: true, DLPTypeHIPAA: true, DLPTypeSecrets: true} |
| 205 | for _, d := range c.DLP.Payloads { |
| 206 | if !validDLP[strings.ToLower(d)] { |
| 207 | return fmt.Errorf("unsupported DLP payload type: '%s'", d) |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | // Validate Carrier Type |
| 212 | validCarriers := map[string]bool{ |
| 213 | CarrierCSV: true, CarrierJSON: true, CarrierSQL: true, CarrierEmail: true, CarrierSyslog: true, CarrierNoise: true, |
| 214 | } |
| 215 | if !validCarriers[strings.ToLower(c.DLP.CarrierType)] { |
| 216 | return fmt.Errorf("unsupported carrier type: '%s'", c.DLP.CarrierType) |
| 217 | } |
| 218 | |
| 219 | // Validate Transforms |
| 220 | validTransforms := map[string]bool{ |
| 221 | TransformRaw: true, TransformBase64: true, TransformHex: true, |
| 222 | TransformGzip: true, TransformZlib: true, TransformZstd: true, TransformAESGCM: true, |
| 223 | } |
| 224 | for _, t := range c.Evasion.Transforms { |
| 225 | if !validTransforms[strings.ToLower(t)] { |
| 226 | return fmt.Errorf("unsupported evasion transform: '%s'", t) |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | // Validate Stego |
| 231 | validStego := map[string]bool{ |
| 232 | StegoNone: true, StegoPNG: true, StegoJPEG: true, StegoPDF: true, StegoZIP: true, |
| 233 | } |
| 234 | if !validStego[strings.ToLower(c.Evasion.Stego)] { |
| 235 | return fmt.Errorf("unsupported stego embedding: '%s'", c.Evasion.Stego) |
| 236 | } |
| 237 | |
| 238 | // Validate TLS Fingerprint |
| 239 | validTLS := map[string]bool{ |
| 240 | TLSFingerprintChrome: true, TLSFingerprintEdge: true, TLSFingerprintSafari: true, |
| 241 | TLSFingerprintFirefox: true, TLSFingerprintCurl: true, TLSFingerprintDefault: true, |
| 242 | } |
| 243 | if !validTLS[strings.ToLower(c.Evasion.TLSFingerprint)] { |
| 244 | return fmt.Errorf("unsupported uTLS fingerprint preset: '%s'", c.Evasion.TLSFingerprint) |
| 245 | } |
| 246 | |
| 247 | // Validate Threads |
| 248 | if c.Traffic.Threads < 1 || c.Traffic.Threads > MaxThreads { |
| 249 | return fmt.Errorf("threads must be between 1 and %d (got %d)", MaxThreads, c.Traffic.Threads) |
| 250 | } |
| 251 | |
| 252 | // Validate Cadence |
| 253 | validCadence := map[string]bool{ |
| 254 | CadenceBurst: true, CadenceSteady: true, CadenceJittered: true, |
| 255 | CadenceOfficeHours: true, CadenceLowAndSlow: true, |
| 256 | } |
| 257 | if !validCadence[strings.ToLower(c.Traffic.Cadence)] { |
| 258 | return fmt.Errorf("unsupported cadence profile: '%s'", c.Traffic.Cadence) |
| 259 | } |
| 260 | |
| 261 | // Validate Cadence Intervals |
| 262 | var minDur, maxDur time.Duration |
| 263 | var hasMin, hasMax bool |
| 264 | |
| 265 | if c.Traffic.IntervalMin != "" { |
| 266 | d, err := ParseDuration(c.Traffic.IntervalMin) |
| 267 | if err != nil { |
| 268 | return fmt.Errorf("invalid interval_min %q: %w", c.Traffic.IntervalMin, err) |
| 269 | } |
| 270 | if d <= 0 { |
| 271 | return fmt.Errorf("interval_min must be greater than zero: %s", c.Traffic.IntervalMin) |
| 272 | } |
| 273 | minDur = d |
| 274 | hasMin = true |
| 275 | } |
| 276 | |
| 277 | if c.Traffic.IntervalMax != "" { |
| 278 | d, err := ParseDuration(c.Traffic.IntervalMax) |
| 279 | if err != nil { |
| 280 | return fmt.Errorf("invalid interval_max %q: %w", c.Traffic.IntervalMax, err) |
| 281 | } |
| 282 | if d <= 0 { |
| 283 | return fmt.Errorf("interval_max must be greater than zero: %s", c.Traffic.IntervalMax) |
| 284 | } |
| 285 | maxDur = d |
| 286 | hasMax = true |
| 287 | } |
| 288 | |
| 289 | if hasMin && hasMax && minDur > maxDur { |
| 290 | return fmt.Errorf("interval_min (%v) cannot be greater than interval_max (%v)", minDur, maxDur) |
| 291 | } |
| 292 | |
| 293 | // Validate Jitter percentage bounds (0.0 <= Jitter <= 1.0 or 0 <= Jitter <= 100) |
| 294 | if c.Traffic.Jitter < 0.0 || (c.Traffic.Jitter > 1.0 && c.Traffic.Jitter > 100.0) { |
| 295 | return fmt.Errorf("jitter percentage must be between 0.0 and 1.0 or 0 and 100 (got %v)", c.Traffic.Jitter) |
| 296 | } |
| 297 | |
| 298 | // Validate Proxy Mode |
| 299 | if c.Proxy.Mode != "auto" && c.Proxy.Mode != "direct" { |
| 300 | if _, err := url.Parse(c.Proxy.Mode); err != nil { |
| 301 | return fmt.Errorf("invalid proxy URL '%s': %w", c.Proxy.Mode, err) |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | // Validate Sweep |
| 306 | if c.Sweep.Concurrency < 1 || c.Sweep.Concurrency > MaxConcurrency { |
| 307 | return fmt.Errorf("concurrency must be between 1 and %d (got %d)", MaxConcurrency, c.Sweep.Concurrency) |
| 308 | } |
| 309 | if c.Sweep.RateLimit < 1 { |
| 310 | return fmt.Errorf("rate_limit must be >= 1 (got %d)", c.Sweep.RateLimit) |
| 311 | } |
| 312 | |
| 313 | return nil |
| 314 | } |
| 315 | |
| 316 | func validatePortsField(ports string) error { |
| 317 | _, err := ParsePortsField(ports) |
| 318 | return err |
| 319 | } |
| 320 | |
| 321 | // ClientFlags captures CLI flag values provided by the Cobra command hierarchy. |
| 322 | type ClientFlags struct { |
| 323 | Server string |
| 324 | Ports string |
| 325 | Protocols []string |
| 326 | DLP []string |
| 327 | CarrierType string |
| 328 | Size string |
| 329 | ChunkSize string |
| 330 | Threads int |
| 331 | Throughput string |
| 332 | Cadence string |
| 333 | IntervalMin string |
| 334 | IntervalMax string |
| 335 | Jitter float64 |
| 336 | Transforms []string |
| 337 | Stego string |
| 338 | TLSFingerprint string |
| 339 | Proxy string |
| 340 | Concurrency int |
| 341 | RateLimit int |
| 342 | OutputJSON string |
| 343 | OutputHTML string |
| 344 | Destroy bool |
| 345 | } |
| 346 | |
| 347 | // MergeWithFlags overlays explicitly provided CLI flags onto ProfileConfig. |
| 348 | // The isChanged function wraps Cobra's cmd.Flags().Changed(flagName). |
| 349 | func (c *ProfileConfig) MergeWithFlags(flags ClientFlags, isChanged func(string) bool) { |
| 350 | if isChanged("server") && flags.Server != "" { |
| 351 | c.Target.Server = flags.Server |
| 352 | } |
| 353 | if isChanged("ports") && flags.Ports != "" { |
| 354 | c.Target.Ports = flags.Ports |
| 355 | } |
| 356 | if isChanged("protocols") && len(flags.Protocols) > 0 { |
| 357 | c.Target.Protocols = flags.Protocols |
| 358 | } |
| 359 | if isChanged("dlp") && len(flags.DLP) > 0 { |
| 360 | c.DLP.Payloads = flags.DLP |
| 361 | } |
| 362 | if isChanged("carrier-type") && flags.CarrierType != "" { |
| 363 | c.DLP.CarrierType = flags.CarrierType |
| 364 | } |
| 365 | if isChanged("size") && flags.Size != "" { |
| 366 | c.DLP.Size = flags.Size |
| 367 | } |
| 368 | if isChanged("chunk-size") && flags.ChunkSize != "" { |
| 369 | c.DLP.ChunkSize = flags.ChunkSize |
| 370 | } |
| 371 | if isChanged("threads") && flags.Threads > 0 { |
| 372 | c.Traffic.Threads = flags.Threads |
| 373 | } |
| 374 | if isChanged("throughput") && flags.Throughput != "" { |
| 375 | c.Traffic.Throughput = flags.Throughput |
| 376 | } |
| 377 | if isChanged("cadence") && flags.Cadence != "" { |
| 378 | c.Traffic.Cadence = flags.Cadence |
| 379 | } |
| 380 | if isChanged("interval-min") && flags.IntervalMin != "" { |
| 381 | c.Traffic.IntervalMin = flags.IntervalMin |
| 382 | } |
| 383 | if isChanged("interval-max") && flags.IntervalMax != "" { |
| 384 | c.Traffic.IntervalMax = flags.IntervalMax |
| 385 | } |
| 386 | if isChanged("jitter") { |
| 387 | c.Traffic.Jitter = flags.Jitter |
| 388 | } |
| 389 | if isChanged("transforms") && len(flags.Transforms) > 0 { |
| 390 | c.Evasion.Transforms = flags.Transforms |
| 391 | } |
| 392 | if isChanged("stego") && flags.Stego != "" { |
| 393 | c.Evasion.Stego = flags.Stego |
| 394 | } |
| 395 | if isChanged("tls-fingerprint") && flags.TLSFingerprint != "" { |
| 396 | c.Evasion.TLSFingerprint = flags.TLSFingerprint |
| 397 | } |
| 398 | if isChanged("proxy") && flags.Proxy != "" { |
| 399 | c.Proxy.Mode = flags.Proxy |
| 400 | } |
| 401 | if isChanged("concurrency") && flags.Concurrency > 0 { |
| 402 | c.Sweep.Concurrency = flags.Concurrency |
| 403 | } |
| 404 | if isChanged("rate-limit") && flags.RateLimit > 0 { |
| 405 | c.Sweep.RateLimit = flags.RateLimit |
| 406 | } |
| 407 | if isChanged("output-json") && flags.OutputJSON != "" { |
| 408 | c.Output.JSONPath = flags.OutputJSON |
| 409 | } |
| 410 | if isChanged("output-html") && flags.OutputHTML != "" { |
| 411 | c.Output.HTMLPath = flags.OutputHTML |
| 412 | } |
| 413 | if isChanged("destroy") { |
| 414 | c.Destroy = flags.Destroy |
| 415 | } |
| 416 | } |
| 417 | |
| 418 | // ServerConfig models server listener configuration parameters for 'contraband server'. |
| 419 | type ServerConfig struct { |
| 420 | Bind string `yaml:"bind" json:"bind"` |
| 421 | Domain string `yaml:"domain" json:"domain"` |
| 422 | Ports string `yaml:"ports" json:"ports"` |
| 423 | Protocols []string `yaml:"protocols" json:"protocols"` |
| 424 | SMBMode string `yaml:"smb_mode" json:"smb_mode"` // 'file-transfer' or 'hash-capture' |
| 425 | CertPath string `yaml:"cert" json:"cert"` |
| 426 | KeyPath string `yaml:"key" json:"key"` |
| 427 | EnableEBPF bool `yaml:"enable_ebpf" json:"enable_ebpf"` |
| 428 | Database string `yaml:"db" json:"db"` |
| 429 | } |
| 430 | |
| 431 | // NewDefaultServerConfig returns standard defaults for the server engine. |
| 432 | func NewDefaultServerConfig() *ServerConfig { |
| 433 | return &ServerConfig{ |
| 434 | Bind: DefaultServerBind, |
| 435 | Domain: "contraband.example.com", |
| 436 | Ports: PortPresetQuick, |
| 437 | Protocols: []string{CategoryAll}, |
| 438 | SMBMode: "hash-capture", |
| 439 | Database: "", // in-memory by default |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | // Validate checks ServerConfig parameters. |
| 444 | func (s *ServerConfig) Validate() error { |
| 445 | if s.Bind == "" { |
| 446 | return errors.New("server bind address cannot be empty") |
| 447 | } |
| 448 | if err := validatePortsField(s.Ports); err != nil { |
| 449 | return fmt.Errorf("invalid server ports: %w", err) |
| 450 | } |
| 451 | if len(s.Protocols) == 0 { |
| 452 | return errors.New("at least one protocol must be specified") |
| 453 | } |
| 454 | if s.SMBMode != "file-transfer" && s.SMBMode != "hash-capture" { |
| 455 | return fmt.Errorf("invalid smb_mode %q (must be 'file-transfer' or 'hash-capture')", s.SMBMode) |
| 456 | } |
| 457 | return nil |
| 458 | } |