From c73fa9d3d1a8db654d7ec51639e25f17fd2c7030 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 7 Dec 2025 15:22:28 -0700 Subject: [PATCH] Add extension enumeration detection and comprehensive SIP protection Major features: - Extension enumeration detection with 3 detection algorithms: - Max unique extensions threshold (default: 20 in 5 min) - Sequential pattern detection (e.g., 100,101,102...) - Rapid-fire detection (many extensions in short window) - Prometheus metrics for all SIP Guardian operations - SQLite persistent storage for bans and attack history - Webhook notifications for ban/unban/suspicious events - GeoIP-based country blocking with continent shortcuts - Per-method rate limiting with token bucket algorithm Bug fixes: - Fix whitelist count always reporting zero in stats - Fix whitelisted connections metric never incrementing - Fix Caddyfile config not being applied to shared guardian New files: - enumeration.go: Extension enumeration detector - enumeration_test.go: 14 comprehensive unit tests - metrics.go: Prometheus metrics handler - storage.go: SQLite persistence layer - webhooks.go: Webhook notification system - geoip.go: MaxMind GeoIP integration - ratelimit.go: Per-method rate limiting Testing: - sandbox/ contains complete Docker Compose test environment - All 14 enumeration tests pass --- Dockerfile | 24 +- Makefile | 180 +++++++- admin.go | 13 +- enumeration.go | 365 ++++++++++++++++ enumeration_test.go | 432 +++++++++++++++++++ geoip.go | 196 +++++++++ go.mod | 168 ++++---- go.sum | 673 +++++++++++++----------------- l4handler.go | 417 +++++++++++++++--- metrics.go | 287 +++++++++++++ ratelimit.go | 360 ++++++++++++++++ registry.go | 172 +++++++- sandbox/Caddyfile | 107 +++++ sandbox/docker-compose.yml | 219 ++++++++++ sandbox/scripts/bruteforce.py | 123 ++++++ sandbox/scripts/valid_register.py | 168 ++++++++ sipguardian.go | 318 +++++++++++++- storage.go | 608 +++++++++++++++++++++++++++ webhooks.go | 344 +++++++++++++++ 19 files changed, 4630 insertions(+), 544 deletions(-) create mode 100644 enumeration.go create mode 100644 enumeration_test.go create mode 100644 geoip.go create mode 100644 metrics.go create mode 100644 ratelimit.go create mode 100644 sandbox/Caddyfile create mode 100644 sandbox/docker-compose.yml create mode 100644 sandbox/scripts/bruteforce.py create mode 100644 sandbox/scripts/valid_register.py create mode 100644 storage.go create mode 100644 webhooks.go diff --git a/Dockerfile b/Dockerfile index 2b97548..cf00c58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,20 @@ -# Build custom Caddy with SIP Guardian, Layer 4, Rate Limiting, and Docker Proxy -FROM caddy:2.8-builder AS builder +# Build custom Caddy with SIP Guardian and Layer 4 support +# Use latest builder with Go 1.25+ for caddy-l4 compatibility +FROM caddy:builder AS builder +# Copy local module source +COPY . /src/caddy-sip-guardian + +# Build Caddy with local module (using replace directive) +# Using latest caddy-l4 which requires Go 1.25+ +WORKDIR /src RUN xcaddy build \ - --with github.com/lucaslorentz/caddy-docker-proxy/v2 \ --with github.com/mholt/caddy-l4 \ - --with github.com/mholt/caddy-ratelimit \ - --with git.supported.systems/rsp2k/caddy-sip-guardian + --with git.supported.systems/rsp2k/caddy-sip-guardian=/src/caddy-sip-guardian -FROM caddy:2.8-alpine +FROM caddy:alpine -COPY --from=builder /usr/bin/caddy /usr/bin/caddy +COPY --from=builder /src/caddy /usr/bin/caddy # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ @@ -18,6 +23,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ EXPOSE 80 443 443/udp 5060 5060/udp 5061 ENTRYPOINT ["caddy"] -# Default: docker-proxy mode (reads Docker labels) -# Override with explicit Caddyfile if needed -CMD ["docker-proxy"] +# Default: run with Caddyfile +CMD ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/Makefile b/Makefile index 13a955d..69a3b51 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,9 @@ -.PHONY: build run stop logs test clean dev +.PHONY: build run stop logs test clean dev sandbox-up sandbox-down sandbox-logs \ + test-bruteforce test-scanner test-valid test-whitelist bans stats + +# ============================================ +# Main Development Targets +# ============================================ # Build the custom Caddy image build: @@ -16,34 +21,169 @@ stop: logs: docker compose logs -f -# Run with mock asterisk for testing -test: - docker compose --profile testing up -d - @echo "Testing SIP Guardian..." - @sleep 3 - @curl -s http://localhost:2019/config/ | jq . - @echo "\nSending test SIP packet..." - @echo -e "OPTIONS sip:test@localhost SIP/2.0\r\nVia: SIP/2.0/UDP 127.0.0.1:5060\r\n\r\n" | nc -u -w1 localhost 5060 - @echo "Check logs for SIP Guardian activity" +# Development mode - rebuild and run +dev: build run logs # Clean up clean: docker compose down -v docker rmi caddy-sip-guardian-caddy 2>/dev/null || true -# Development mode - rebuild and run -dev: build run logs +# ============================================ +# Sandbox Testing Environment +# ============================================ + +# Start the full testing sandbox (FreePBX + Caddy + test tools) +sandbox-up: + @echo "Starting SIP Guardian testing sandbox..." + cd sandbox && docker compose up -d + @echo "" + @echo "Sandbox is starting. FreePBX takes a few minutes to initialize." + @echo "Services:" + @echo " - Caddy (SIP Guardian): localhost:5060 (UDP/TCP), localhost:5061 (TLS)" + @echo " - Admin API: http://localhost:2020/api/sip-guardian/" + @echo " - FreePBX Web: http://localhost:80 (once ready)" + @echo "" + @echo "Run 'make sandbox-logs' to monitor startup" + +# Stop sandbox +sandbox-down: + cd sandbox && docker compose down + +# Stop sandbox and remove volumes +sandbox-clean: + cd sandbox && docker compose down -v + +# View sandbox logs +sandbox-logs: + cd sandbox && docker compose logs -f + +# View only Caddy logs +caddy-logs: + cd sandbox && docker compose logs -f caddy + +# Start testing containers +sandbox-test-containers: + cd sandbox && docker compose --profile testing up -d + +# ============================================ +# Attack Simulation Tests +# ============================================ + +# Test brute force attack (should trigger ban) +test-bruteforce: + @echo "Starting brute force simulation..." + cd sandbox && docker compose --profile testing up -d bruteforcer + cd sandbox && docker compose exec bruteforcer python /scripts/bruteforce.py caddy -e 100-105 -c 5 -d 0.2 + @echo "" + @echo "Check ban list:" + @curl -s http://localhost:2020/api/sip-guardian/bans | jq . + +# Test scanner detection (sipvicious patterns) +test-scanner: + @echo "Starting scanner simulation..." + cd sandbox && docker compose --profile testing up -d attacker + cd sandbox && docker compose exec attacker bash -c "pip install -q sipvicious && sipvicious_svwar -e100-110 caddy" + @echo "" + @echo "Check ban list:" + @curl -s http://localhost:2020/api/sip-guardian/bans | jq . + +# Test valid registration (should NOT be blocked) +test-valid: + @echo "Testing valid registration..." + cd sandbox && docker compose --profile testing up -d client + cd sandbox && docker compose exec client python3 /scripts/valid_register.py caddy -e 100 -s password123 -r 3 + @echo "" + @echo "Stats (should show no bans for legitimate client):" + @curl -s http://localhost:2020/api/sip-guardian/stats | jq . + +# Test whitelist functionality +test-whitelist: + @echo "Testing whitelist bypass..." + @echo "Whitelisted client (172.28.0.50) sending many requests:" + cd sandbox && docker compose --profile testing up -d client + cd sandbox && docker compose exec client sh -c 'for i in $$(seq 1 20); do echo -e "REGISTER sip:caddy SIP/2.0\r\n\r\n" | nc -u -w1 caddy 5060; done' + @echo "" + @echo "Ban list (should NOT contain 172.28.0.50):" + @curl -s http://localhost:2020/api/sip-guardian/bans | jq . + +# Send raw SIP OPTIONS (quick test) +test-sip-options: + @echo "Sending SIP OPTIONS request..." + @echo -e "OPTIONS sip:test@localhost SIP/2.0\r\nVia: SIP/2.0/UDP 127.0.0.1:5060;branch=z9hG4bK-test\r\nFrom: ;tag=123\r\nTo: \r\nCall-ID: test-call@localhost\r\nCSeq: 1 OPTIONS\r\nMax-Forwards: 70\r\nContent-Length: 0\r\n\r\n" | nc -u -w2 localhost 5060 + +# ============================================ +# Admin API Operations +# ============================================ # Check ban list via admin API bans: - @curl -s http://localhost:2019/load | jq . - -# Add test ban -test-ban: - @curl -X POST http://localhost:2019/api/sip-guardian/ban/192.168.1.100 \ - -H "Content-Type: application/json" \ - -d '{"reason": "test_ban"}' | jq . + @curl -s http://localhost:2020/api/sip-guardian/bans | jq . # View stats stats: - @curl -s http://localhost:2019/api/sip-guardian/stats | jq . + @curl -s http://localhost:2020/api/sip-guardian/stats | jq . + +# Add test ban +test-ban: + @curl -X POST http://localhost:2020/api/sip-guardian/ban/192.168.1.100 \ + -H "Content-Type: application/json" \ + -d '{"reason": "test_ban"}' | jq . + +# Remove test ban +test-unban: + @curl -X POST http://localhost:2020/api/sip-guardian/unban/192.168.1.100 | jq . + +# Health check +health: + @curl -s http://localhost:2020/health + +# ============================================ +# Debugging +# ============================================ + +# Start tcpdump container to capture SIP traffic +tcpdump: + cd sandbox && docker compose --profile debug up -d tcpdump + cd sandbox && docker compose logs -f tcpdump + +# Shell into Caddy container +caddy-shell: + cd sandbox && docker compose exec caddy sh + +# Shell into FreePBX container +freepbx-shell: + cd sandbox && docker compose exec freepbx bash + +# View Caddy config +caddy-config: + @curl -s http://localhost:2019/config/ | jq . + +# ============================================ +# Help +# ============================================ + +help: + @echo "Caddy SIP Guardian - Development Makefile" + @echo "" + @echo "Main targets:" + @echo " build - Build Docker image" + @echo " dev - Build, run, and tail logs" + @echo " clean - Stop and remove volumes" + @echo "" + @echo "Sandbox targets:" + @echo " sandbox-up - Start full testing sandbox (FreePBX + Caddy)" + @echo " sandbox-down - Stop sandbox" + @echo " sandbox-logs - View sandbox logs" + @echo "" + @echo "Test targets:" + @echo " test-bruteforce - Simulate brute force attack (should ban)" + @echo " test-scanner - Simulate sipvicious scanner (should ban)" + @echo " test-valid - Test legitimate registration (should pass)" + @echo " test-whitelist - Test whitelist bypass" + @echo "" + @echo "Admin targets:" + @echo " bans - List banned IPs" + @echo " stats - View statistics" + @echo " test-ban - Add test ban" + @echo " test-unban - Remove test ban" diff --git a/admin.go b/admin.go index 26325ff..f7dfb10 100644 --- a/admin.go +++ b/admin.go @@ -14,6 +14,8 @@ import ( func init() { caddy.RegisterModule(AdminHandler{}) httpcaddyfile.RegisterHandlerDirective("sip_guardian_admin", parseSIPGuardianAdmin) + // Register handler ordering so it can be used directly in handle blocks + httpcaddyfile.RegisterDirectiveOrder("sip_guardian_admin", httpcaddyfile.Before, "respond") } // parseSIPGuardianAdmin parses the sip_guardian_admin directive @@ -36,10 +38,13 @@ func (AdminHandler) CaddyModule() caddy.ModuleInfo { } func (h *AdminHandler) Provision(ctx caddy.Context) error { - // Get the shared guardian instance - // In production, this would use proper module loading - h.guardian = &SIPGuardian{} - return h.guardian.Provision(ctx) + // Get the shared guardian instance from the global registry + guardian, err := GetOrCreateGuardian(ctx, "default") + if err != nil { + return err + } + h.guardian = guardian + return nil } // ServeHTTP handles admin API requests diff --git a/enumeration.go b/enumeration.go new file mode 100644 index 0000000..749f8df --- /dev/null +++ b/enumeration.go @@ -0,0 +1,365 @@ +package sipguardian + +import ( + "sort" + "strconv" + "sync" + "time" + + "go.uber.org/zap" +) + +// EnumerationConfig holds configuration for extension enumeration detection +type EnumerationConfig struct { + // MaxExtensions is the maximum unique extensions an IP can probe before ban + MaxExtensions int `json:"max_extensions,omitempty"` + + // ExtensionWindow is the time window for counting unique extensions + ExtensionWindow time.Duration `json:"extension_window,omitempty"` + + // SequentialThreshold triggers on N+ consecutive extensions (100,101,102...) + SequentialThreshold int `json:"sequential_threshold,omitempty"` + + // RapidFireCount triggers on N extensions in RapidFireWindow + RapidFireCount int `json:"rapid_fire_count,omitempty"` + + // RapidFireWindow is the time window for rapid-fire detection + RapidFireWindow time.Duration `json:"rapid_fire_window,omitempty"` + + // EnumBanTime is ban duration for enumeration attacks (longer than normal) + EnumBanTime time.Duration `json:"enum_ban_time,omitempty"` + + // ExemptExtensions are extensions that don't count toward enumeration + ExemptExtensions []string `json:"exempt_extensions,omitempty"` +} + +// DefaultEnumerationConfig returns sensible defaults +func DefaultEnumerationConfig() EnumerationConfig { + return EnumerationConfig{ + MaxExtensions: 20, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 5, + RapidFireCount: 10, + RapidFireWindow: 30 * time.Second, + EnumBanTime: 2 * time.Hour, + ExemptExtensions: []string{}, + } +} + +// EnumerationDetector detects extension scanning attacks +type EnumerationDetector struct { + config EnumerationConfig + tracker map[string]*ExtensionAttempts + exemptSet map[string]bool + logger *zap.Logger + mu sync.RWMutex +} + +// ExtensionAttempts tracks extension attempts for a single IP +type ExtensionAttempts struct { + extensions map[string]time.Time // extension -> last attempt time + ordered []extAttempt // time-ordered for pattern detection + firstSeen time.Time + lastSeen time.Time + flaggedSeq bool // already detected sequential pattern + mu sync.Mutex +} + +type extAttempt struct { + extension string + timestamp time.Time + numeric int // -1 if non-numeric +} + +// EnumerationResult contains detection results +type EnumerationResult struct { + Detected bool + Reason string + UniqueCount int + SeqStart int // start of sequential range (if detected) + SeqEnd int // end of sequential range (if detected) + Extensions []string +} + +// Global detector instance +var ( + globalEnumDetector *EnumerationDetector + enumDetectorMu sync.Mutex +) + +// GetEnumerationDetector returns the global enumeration detector +func GetEnumerationDetector(logger *zap.Logger) *EnumerationDetector { + enumDetectorMu.Lock() + defer enumDetectorMu.Unlock() + + if globalEnumDetector == nil { + globalEnumDetector = NewEnumerationDetector(logger, DefaultEnumerationConfig()) + } + return globalEnumDetector +} + +// SetEnumerationConfig updates the global detector configuration +func SetEnumerationConfig(config EnumerationConfig) { + enumDetectorMu.Lock() + defer enumDetectorMu.Unlock() + + if globalEnumDetector != nil { + globalEnumDetector.config = config + // Rebuild exempt set + globalEnumDetector.exemptSet = make(map[string]bool) + for _, ext := range config.ExemptExtensions { + globalEnumDetector.exemptSet[ext] = true + } + } +} + +// NewEnumerationDetector creates a new detector +func NewEnumerationDetector(logger *zap.Logger, config EnumerationConfig) *EnumerationDetector { + exemptSet := make(map[string]bool) + for _, ext := range config.ExemptExtensions { + exemptSet[ext] = true + } + + return &EnumerationDetector{ + config: config, + tracker: make(map[string]*ExtensionAttempts), + exemptSet: exemptSet, + logger: logger, + } +} + +// RecordAttempt records an extension attempt and returns detection result +func (ed *EnumerationDetector) RecordAttempt(ip, extension string) EnumerationResult { + // Skip exempt extensions + if ed.exemptSet[extension] { + return EnumerationResult{Detected: false} + } + + ed.mu.Lock() + attempts, exists := ed.tracker[ip] + if !exists { + attempts = &ExtensionAttempts{ + extensions: make(map[string]time.Time), + ordered: make([]extAttempt, 0), + firstSeen: time.Now(), + } + ed.tracker[ip] = attempts + } + ed.mu.Unlock() + + return attempts.record(extension, ed.config) +} + +// record adds an attempt and checks for enumeration patterns +func (ea *ExtensionAttempts) record(extension string, config EnumerationConfig) EnumerationResult { + ea.mu.Lock() + defer ea.mu.Unlock() + + now := time.Now() + ea.lastSeen = now + + // Clean old entries + ea.cleanup(config.ExtensionWindow) + + // Parse as numeric if possible + numeric := -1 + if n, err := strconv.Atoi(extension); err == nil { + numeric = n + } + + // Record the attempt (update timestamp if already seen) + if _, exists := ea.extensions[extension]; !exists { + ea.extensions[extension] = now + ea.ordered = append(ea.ordered, extAttempt{ + extension: extension, + timestamp: now, + numeric: numeric, + }) + } else { + ea.extensions[extension] = now + } + + // Check detection rules + result := EnumerationResult{ + UniqueCount: len(ea.extensions), + Extensions: ea.getExtensionList(), + } + + // Rule 1: Too many unique extensions + if len(ea.extensions) >= config.MaxExtensions { + result.Detected = true + result.Reason = "extension_count_exceeded" + return result + } + + // Rule 2: Sequential pattern detection + if !ea.flaggedSeq && len(ea.extensions) >= config.SequentialThreshold { + if detected, start, end := ea.detectSequentialPattern(config.SequentialThreshold); detected { + ea.flaggedSeq = true + result.Detected = true + result.Reason = "sequential_enumeration" + result.SeqStart = start + result.SeqEnd = end + return result + } + } + + // Rule 3: Rapid-fire detection + if detected := ea.detectRapidFire(config.RapidFireCount, config.RapidFireWindow); detected { + result.Detected = true + result.Reason = "rapid_fire_enumeration" + return result + } + + return result +} + +// detectSequentialPattern finds consecutive numeric sequences +func (ea *ExtensionAttempts) detectSequentialPattern(threshold int) (bool, int, int) { + var nums []int + for ext := range ea.extensions { + if n, err := strconv.Atoi(ext); err == nil { + nums = append(nums, n) + } + } + + if len(nums) < threshold { + return false, 0, 0 + } + + sort.Ints(nums) + + // Find longest consecutive sequence + maxRun, runStart := 1, nums[0] + currentRun, currentStart := 1, nums[0] + + for i := 1; i < len(nums); i++ { + if nums[i] == nums[i-1]+1 { + currentRun++ + if currentRun > maxRun { + maxRun = currentRun + runStart = currentStart + } + } else { + currentRun = 1 + currentStart = nums[i] + } + } + + return maxRun >= threshold, runStart, runStart + maxRun - 1 +} + +// detectRapidFire checks for many extensions in a short time +func (ea *ExtensionAttempts) detectRapidFire(count int, window time.Duration) bool { + cutoff := time.Now().Add(-window) + seen := make(map[string]bool) + + for _, attempt := range ea.ordered { + if attempt.timestamp.After(cutoff) && !seen[attempt.extension] { + seen[attempt.extension] = true + } + } + + return len(seen) >= count +} + +// cleanup removes old entries outside the window +func (ea *ExtensionAttempts) cleanup(window time.Duration) { + cutoff := time.Now().Add(-window) + + // Clean extensions map + for ext, ts := range ea.extensions { + if ts.Before(cutoff) { + delete(ea.extensions, ext) + } + } + + // Clean ordered slice + newOrdered := make([]extAttempt, 0, len(ea.ordered)) + for _, a := range ea.ordered { + if a.timestamp.After(cutoff) { + newOrdered = append(newOrdered, a) + } + } + ea.ordered = newOrdered + + // Reset sequential flag if we've cleaned enough entries + if len(ea.extensions) < 3 { + ea.flaggedSeq = false + } +} + +func (ea *ExtensionAttempts) getExtensionList() []string { + exts := make([]string, 0, len(ea.extensions)) + for ext := range ea.extensions { + exts = append(exts, ext) + } + return exts +} + +// Cleanup removes stale IP entries from the detector +func (ed *EnumerationDetector) Cleanup() { + ed.mu.Lock() + defer ed.mu.Unlock() + + cutoff := time.Now().Add(-ed.config.ExtensionWindow * 2) + + for ip, attempts := range ed.tracker { + attempts.mu.Lock() + if attempts.lastSeen.Before(cutoff) { + delete(ed.tracker, ip) + } + attempts.mu.Unlock() + } +} + +// GetStats returns detector statistics +func (ed *EnumerationDetector) GetStats() map[string]interface{} { + ed.mu.RLock() + defer ed.mu.RUnlock() + + // Count total tracked extensions across all IPs + totalExtensions := 0 + for _, attempts := range ed.tracker { + attempts.mu.Lock() + totalExtensions += len(attempts.extensions) + attempts.mu.Unlock() + } + + return map[string]interface{}{ + "tracked_ips": len(ed.tracker), + "total_extensions": totalExtensions, + "max_extensions": ed.config.MaxExtensions, + "sequential_threshold": ed.config.SequentialThreshold, + "rapid_fire_count": ed.config.RapidFireCount, + "extension_window": ed.config.ExtensionWindow.String(), + "rapid_fire_window": ed.config.RapidFireWindow.String(), + } +} + +// GetIPAttempts returns the extension attempts for a specific IP (for debugging/admin) +func (ed *EnumerationDetector) GetIPAttempts(ip string) *EnumerationResult { + ed.mu.RLock() + attempts, exists := ed.tracker[ip] + ed.mu.RUnlock() + + if !exists { + return nil + } + + attempts.mu.Lock() + defer attempts.mu.Unlock() + + return &EnumerationResult{ + Detected: false, + UniqueCount: len(attempts.extensions), + Extensions: attempts.getExtensionList(), + } +} + +// ResetIP clears tracking for a specific IP (for admin use) +func (ed *EnumerationDetector) ResetIP(ip string) { + ed.mu.Lock() + defer ed.mu.Unlock() + delete(ed.tracker, ip) +} diff --git a/enumeration_test.go b/enumeration_test.go new file mode 100644 index 0000000..95169e5 --- /dev/null +++ b/enumeration_test.go @@ -0,0 +1,432 @@ +package sipguardian + +import ( + "strconv" + "testing" + "time" + + "go.uber.org/zap" +) + +func newTestDetector(config EnumerationConfig) *EnumerationDetector { + logger := zap.NewNop() + return NewEnumerationDetector(logger, config) +} + +func TestDefaultConfig(t *testing.T) { + config := DefaultEnumerationConfig() + + if config.MaxExtensions != 20 { + t.Errorf("Expected MaxExtensions=20, got %d", config.MaxExtensions) + } + if config.ExtensionWindow != 5*time.Minute { + t.Errorf("Expected ExtensionWindow=5m, got %v", config.ExtensionWindow) + } + if config.SequentialThreshold != 5 { + t.Errorf("Expected SequentialThreshold=5, got %d", config.SequentialThreshold) + } + if config.RapidFireCount != 10 { + t.Errorf("Expected RapidFireCount=10, got %d", config.RapidFireCount) + } + if config.RapidFireWindow != 30*time.Second { + t.Errorf("Expected RapidFireWindow=30s, got %v", config.RapidFireWindow) + } +} + +func TestMaxExtensionsDetection(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 5, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 100, // Disable sequential detection + RapidFireCount: 100, // Disable rapid-fire detection + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.100" + + // Record 4 different extensions - should not trigger + for i := 0; i < 4; i++ { + ext := strconv.Itoa(1000 + i*100) // Non-sequential: 1000, 1100, 1200, 1300 + result := detector.RecordAttempt(ip, ext) + if result.Detected { + t.Errorf("Should not detect on extension %d (count=%d)", i+1, result.UniqueCount) + } + } + + // 5th unique extension should trigger + result := detector.RecordAttempt(ip, "2000") + if !result.Detected { + t.Error("Should detect when max_extensions reached") + } + if result.Reason != "extension_count_exceeded" { + t.Errorf("Expected reason 'extension_count_exceeded', got '%s'", result.Reason) + } + if result.UniqueCount != 5 { + t.Errorf("Expected unique_count=5, got %d", result.UniqueCount) + } +} + +func TestSequentialPatternDetection(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 100, // High to avoid triggering count-based + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 5, + RapidFireCount: 100, // Disable rapid-fire + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.101" + + // Record sequential extensions: 100, 101, 102, 103 + for i := 100; i <= 103; i++ { + result := detector.RecordAttempt(ip, strconv.Itoa(i)) + if result.Detected { + t.Errorf("Should not detect on extension %d", i) + } + } + + // 5th sequential should trigger + result := detector.RecordAttempt(ip, "104") + if !result.Detected { + t.Error("Should detect sequential pattern at 5 consecutive") + } + if result.Reason != "sequential_enumeration" { + t.Errorf("Expected reason 'sequential_enumeration', got '%s'", result.Reason) + } + if result.SeqStart != 100 || result.SeqEnd != 104 { + t.Errorf("Expected SeqStart=100, SeqEnd=104, got %d-%d", result.SeqStart, result.SeqEnd) + } +} + +func TestSequentialPatternGaps(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 100, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 5, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.102" + + // Non-sequential extensions with gaps + extensions := []string{"100", "102", "104", "106", "108"} + for _, ext := range extensions { + result := detector.RecordAttempt(ip, ext) + if result.Detected && result.Reason == "sequential_enumeration" { + t.Errorf("Should not detect sequential pattern for non-consecutive: %s", ext) + } + } +} + +func TestRapidFireDetection(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 100, // High to avoid triggering count-based + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 100, // Disable sequential + RapidFireCount: 5, + RapidFireWindow: 1 * time.Second, // Short window for testing + } + detector := newTestDetector(config) + + ip := "192.168.1.103" + + // Record 5 different extensions rapidly (within the window) + for i := 0; i < 4; i++ { + ext := strconv.Itoa(1000 + i*100) // Non-sequential + result := detector.RecordAttempt(ip, ext) + if result.Detected && result.Reason == "rapid_fire_enumeration" { + t.Errorf("Should not detect rapid-fire on attempt %d", i+1) + } + } + + // 5th should trigger rapid-fire + result := detector.RecordAttempt(ip, "5000") + if !result.Detected { + t.Error("Should detect rapid-fire pattern") + } + if result.Reason != "rapid_fire_enumeration" { + t.Errorf("Expected reason 'rapid_fire_enumeration', got '%s'", result.Reason) + } +} + +func TestExemptExtensions(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 3, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 3, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + ExemptExtensions: []string{"100", "200", "emergency"}, + } + detector := newTestDetector(config) + + ip := "192.168.1.104" + + // Exempt extensions should not count + exemptExts := []string{"100", "200", "emergency"} + for _, ext := range exemptExts { + result := detector.RecordAttempt(ip, ext) + if result.Detected { + t.Errorf("Exempt extension '%s' should not trigger detection", ext) + } + } + + // Non-exempt extensions should still count + result := detector.RecordAttempt(ip, "1001") + if result.Detected { + t.Error("First non-exempt should not trigger") + } + result = detector.RecordAttempt(ip, "1002") + if result.Detected { + t.Error("Second non-exempt should not trigger") + } + result = detector.RecordAttempt(ip, "1003") + if !result.Detected { + t.Error("Third non-exempt should trigger (max_extensions=3)") + } +} + +func TestDuplicateExtensions(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 3, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 100, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.105" + + // Record same extension multiple times - should only count as 1 + for i := 0; i < 10; i++ { + result := detector.RecordAttempt(ip, "1000") + if result.Detected { + t.Error("Duplicate extensions should not trigger detection") + } + if result.UniqueCount != 1 { + t.Errorf("Expected unique_count=1 for duplicates, got %d", result.UniqueCount) + } + } +} + +func TestMultipleIPsIsolation(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 3, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 100, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip1 := "192.168.1.106" + ip2 := "192.168.1.107" + + // Record extensions for IP1 + for i := 0; i < 2; i++ { + detector.RecordAttempt(ip1, strconv.Itoa(1000+i)) + } + + // Record extensions for IP2 - should start fresh + result := detector.RecordAttempt(ip2, "2000") + if result.UniqueCount != 1 { + t.Errorf("IP2 should have independent count, expected 1, got %d", result.UniqueCount) + } + + // IP1's 3rd should trigger + result = detector.RecordAttempt(ip1, "1002") + if !result.Detected { + t.Error("IP1 should trigger on 3rd unique extension") + } + + // IP2 should still be fine + result = detector.RecordAttempt(ip2, "2001") + if result.Detected { + t.Error("IP2 should not trigger on 2nd unique extension") + } +} + +func TestGetStats(t *testing.T) { + config := DefaultEnumerationConfig() + detector := newTestDetector(config) + + // Initial stats + stats := detector.GetStats() + if stats["tracked_ips"].(int) != 0 { + t.Errorf("Expected tracked_ips=0 initially, got %d", stats["tracked_ips"]) + } + + // Record some attempts + detector.RecordAttempt("192.168.1.1", "1000") + detector.RecordAttempt("192.168.1.2", "2000") + + stats = detector.GetStats() + if stats["tracked_ips"].(int) != 2 { + t.Errorf("Expected tracked_ips=2, got %d", stats["tracked_ips"]) + } + if stats["total_extensions"].(int) != 2 { + t.Errorf("Expected total_extensions=2, got %d", stats["total_extensions"]) + } +} + +func TestGetIPAttempts(t *testing.T) { + config := DefaultEnumerationConfig() + detector := newTestDetector(config) + + ip := "192.168.1.108" + + // No attempts yet + result := detector.GetIPAttempts(ip) + if result != nil { + t.Error("Expected nil for non-tracked IP") + } + + // Record some attempts + detector.RecordAttempt(ip, "1000") + detector.RecordAttempt(ip, "1001") + detector.RecordAttempt(ip, "1002") + + result = detector.GetIPAttempts(ip) + if result == nil { + t.Fatal("Expected result for tracked IP") + } + if result.UniqueCount != 3 { + t.Errorf("Expected unique_count=3, got %d", result.UniqueCount) + } + if len(result.Extensions) != 3 { + t.Errorf("Expected 3 extensions, got %d", len(result.Extensions)) + } +} + +func TestResetIP(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 3, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 100, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.109" + + // Record 2 extensions + detector.RecordAttempt(ip, "1000") + detector.RecordAttempt(ip, "1001") + + // Reset the IP + detector.ResetIP(ip) + + // Should start fresh - no detection yet + result := detector.RecordAttempt(ip, "2000") + if result.UniqueCount != 1 { + t.Errorf("After reset, expected unique_count=1, got %d", result.UniqueCount) + } +} + +func TestNonNumericExtensions(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 100, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 3, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.110" + + // Non-numeric extensions should not trigger sequential detection + nonNumeric := []string{"sales", "support", "main", "fax", "reception"} + for _, ext := range nonNumeric { + result := detector.RecordAttempt(ip, ext) + if result.Detected && result.Reason == "sequential_enumeration" { + t.Errorf("Non-numeric '%s' should not trigger sequential detection", ext) + } + } +} + +func TestMixedNumericNonNumeric(t *testing.T) { + config := EnumerationConfig{ + MaxExtensions: 100, + ExtensionWindow: 5 * time.Minute, + SequentialThreshold: 5, + RapidFireCount: 100, + RapidFireWindow: 30 * time.Second, + } + detector := newTestDetector(config) + + ip := "192.168.1.111" + + // Mix of numeric sequential with non-numeric interruptions + // Still should detect sequence in numeric ones + extensions := []string{"100", "main", "101", "support", "102", "sales", "103", "104"} + var detectedSeq bool + for _, ext := range extensions { + result := detector.RecordAttempt(ip, ext) + if result.Detected && result.Reason == "sequential_enumeration" { + detectedSeq = true + } + } + if !detectedSeq { + t.Error("Should detect 5 sequential numeric extensions even with non-numeric mixed in") + } +} + +func TestExtractTargetExtension(t *testing.T) { + testCases := []struct { + name string + data string + expected string + }{ + { + name: "REGISTER with extension", + data: "REGISTER sip:1001@example.com SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.1\r\n", + expected: "1001", + }, + { + name: "INVITE with extension", + data: "INVITE sip:2000@pbx.local SIP/2.0\r\nFrom: \r\n", + expected: "2000", + }, + { + name: "OPTIONS with extension", + data: "OPTIONS sip:100@domain.com SIP/2.0\r\n", + expected: "100", + }, + { + name: "Extension too long (should skip)", + data: "REGISTER sip:verylongextensionname@example.com SIP/2.0\r\n", + expected: "", + }, + { + name: "Domain-like user (should skip)", + data: "REGISTER sip:example.com@example.com SIP/2.0\r\n", + expected: "", + }, + { + name: "BYE method (not tracked)", + data: "BYE sip:1001@example.com SIP/2.0\r\n", + expected: "", + }, + { + name: "Fallback to To header", + data: "ACK sip:anything@example.com SIP/2.0\r\nTo: \r\n", + expected: "500", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := ExtractTargetExtension([]byte(tc.data)) + if result != tc.expected { + t.Errorf("Expected '%s', got '%s'", tc.expected, result) + } + }) + } +} diff --git a/geoip.go b/geoip.go new file mode 100644 index 0000000..09c224c --- /dev/null +++ b/geoip.go @@ -0,0 +1,196 @@ +package sipguardian + +import ( + "fmt" + "net" + "sync" + + "github.com/oschwald/maxminddb-golang" +) + +// GeoIPLookup provides IP to country lookup using MaxMind databases +type GeoIPLookup struct { + db *maxminddb.Reader + mu sync.RWMutex +} + +// GeoIPRecord represents the data we extract from MaxMind database +type GeoIPRecord struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"country"` + City struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"city"` + Continent struct { + Code string `maxminddb:"code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"continent"` +} + +// NewGeoIPLookup creates a new GeoIP lookup from a MaxMind database file +// Supports both GeoLite2-Country and GeoLite2-City databases +func NewGeoIPLookup(path string) (*GeoIPLookup, error) { + db, err := maxminddb.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open GeoIP database: %w", err) + } + + return &GeoIPLookup{ + db: db, + }, nil +} + +// LookupCountry returns the ISO country code for an IP address +func (g *GeoIPLookup) LookupCountry(ipStr string) (string, error) { + g.mu.RLock() + defer g.mu.RUnlock() + + ip := net.ParseIP(ipStr) + if ip == nil { + return "", fmt.Errorf("invalid IP address: %s", ipStr) + } + + var record GeoIPRecord + err := g.db.Lookup(ip, &record) + if err != nil { + return "", fmt.Errorf("lookup failed: %w", err) + } + + return record.Country.ISOCode, nil +} + +// LookupFull returns full GeoIP information for an IP address +func (g *GeoIPLookup) LookupFull(ipStr string) (*GeoIPRecord, error) { + g.mu.RLock() + defer g.mu.RUnlock() + + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", ipStr) + } + + var record GeoIPRecord + err := g.db.Lookup(ip, &record) + if err != nil { + return nil, fmt.Errorf("lookup failed: %w", err) + } + + return &record, nil +} + +// Close closes the database +func (g *GeoIPLookup) Close() error { + g.mu.Lock() + defer g.mu.Unlock() + return g.db.Close() +} + +// CountryName returns the English name for a country code +func (g *GeoIPLookup) CountryName(ipStr string) (string, error) { + record, err := g.LookupFull(ipStr) + if err != nil { + return "", err + } + + if name, ok := record.Country.Names["en"]; ok { + return name, nil + } + + return record.Country.ISOCode, nil +} + +// IsPrivate checks if an IP is in a private/reserved range +func IsPrivateIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + + // Private IPv4 ranges + privateBlocks := []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "127.0.0.0/8", + "169.254.0.0/16", // Link-local + "100.64.0.0/10", // Carrier-grade NAT + } + + // Private IPv6 ranges + if ip.To4() == nil { + privateBlocks = append(privateBlocks, + "::1/128", // Loopback + "fc00::/7", // Unique local + "fe80::/10", // Link-local + ) + } + + for _, block := range privateBlocks { + _, cidr, err := net.ParseCIDR(block) + if err != nil { + continue + } + if cidr.Contains(ip) { + return true + } + } + + return false +} + +// Continent codes for grouping countries +var ContinentCountries = map[string][]string{ + "AF": { // Africa + "AO", "BF", "BI", "BJ", "BW", "CD", "CF", "CG", "CI", "CM", + "CV", "DJ", "DZ", "EG", "EH", "ER", "ET", "GA", "GH", "GM", + "GN", "GQ", "GW", "KE", "KM", "LR", "LS", "LY", "MA", "MG", + "ML", "MR", "MU", "MW", "MZ", "NA", "NE", "NG", "RE", "RW", + "SC", "SD", "SH", "SL", "SN", "SO", "SS", "ST", "SZ", "TD", + "TG", "TN", "TZ", "UG", "YT", "ZA", "ZM", "ZW", + }, + "AS": { // Asia + "AE", "AF", "AM", "AZ", "BD", "BH", "BN", "BT", "CN", "CY", + "GE", "HK", "ID", "IL", "IN", "IQ", "IR", "JO", "JP", "KG", + "KH", "KP", "KR", "KW", "KZ", "LA", "LB", "LK", "MM", "MN", + "MO", "MV", "MY", "NP", "OM", "PH", "PK", "PS", "QA", "SA", + "SG", "SY", "TH", "TJ", "TL", "TM", "TR", "TW", "UZ", "VN", + "YE", + }, + "EU": { // Europe + "AD", "AL", "AT", "AX", "BA", "BE", "BG", "BY", "CH", "CZ", + "DE", "DK", "EE", "ES", "FI", "FO", "FR", "GB", "GG", "GI", + "GR", "HR", "HU", "IE", "IM", "IS", "IT", "JE", "LI", "LT", + "LU", "LV", "MC", "MD", "ME", "MK", "MT", "NL", "NO", "PL", + "PT", "RO", "RS", "RU", "SE", "SI", "SJ", "SK", "SM", "UA", + "VA", "XK", + }, + "NA": { // North America + "AG", "AI", "AW", "BB", "BL", "BM", "BQ", "BS", "BZ", "CA", + "CR", "CU", "CW", "DM", "DO", "GD", "GL", "GP", "GT", "HN", + "HT", "JM", "KN", "KY", "LC", "MF", "MQ", "MS", "MX", "NI", + "PA", "PM", "PR", "SV", "SX", "TC", "TT", "US", "VC", "VG", + "VI", + }, + "SA": { // South America + "AR", "BO", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PE", + "PY", "SR", "UY", "VE", + }, + "OC": { // Oceania + "AS", "AU", "CK", "FJ", "FM", "GU", "KI", "MH", "MP", "NC", + "NF", "NR", "NU", "NZ", "PF", "PG", "PN", "PW", "SB", "TK", + "TO", "TV", "VU", "WF", "WS", + }, + "AN": { // Antarctica + "AQ", "GS", "HM", "TF", + }, +} + +// ExpandContinentCode expands a continent code to its country codes +func ExpandContinentCode(code string) []string { + if countries, ok := ContinentCountries[code]; ok { + return countries + } + return nil +} diff --git a/go.mod b/go.mod index eb9f00f..5050be8 100644 --- a/go.mod +++ b/go.mod @@ -1,117 +1,137 @@ module git.supported.systems/rsp2k/caddy-sip-guardian -go 1.22.0 +go 1.25 require ( - github.com/caddyserver/caddy/v2 v2.8.4 - github.com/mholt/caddy-l4 v0.0.0-20241104153248-ec8fae209322 - go.uber.org/zap v1.27.0 + github.com/caddyserver/caddy/v2 v2.10.2 + github.com/mholt/caddy-l4 v0.0.0-20251204151317-049ea4dcfaf0 + github.com/oschwald/maxminddb-golang v1.13.1 + github.com/prometheus/client_golang v1.23.0 + go.uber.org/zap v1.27.1 + modernc.org/sqlite v1.38.0 ) require ( + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/KimMachineGun/automemlimit v0.7.4 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/caddyserver/certmagic v0.21.3 // indirect + github.com/caddyserver/certmagic v0.24.0 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/ccoveille/go-safecast v1.6.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/coreos/go-oidc/v3 v3.14.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-kit/kit v0.13.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang/glog v1.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/cel-go v0.20.1 // indirect - github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.14.0 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/klauspost/compress v1.17.8 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/libdns/libdns v0.2.2 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/libdns/libdns v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/mholt/acmez/v2 v2.0.1 // indirect - github.com/miekg/dns v1.1.62 // indirect + github.com/mholt/acmez/v3 v3.1.2 // indirect + github.com/miekg/dns v1.1.68 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/onsi/ginkgo/v2 v2.15.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/quic-go v0.44.0 // indirect - github.com/rs/xid v1.5.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.57.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/slackhq/nebula v1.7.2 // indirect - github.com/smallstep/certificates v0.26.1 // indirect - github.com/smallstep/nosql v0.6.1 // indirect - github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect - github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect + github.com/slackhq/nebula v1.9.7 // indirect + github.com/smallstep/certificates v0.28.4 // indirect + github.com/smallstep/cli-utils v0.12.1 // indirect + github.com/smallstep/linkedca v0.23.0 // indirect + github.com/smallstep/nosql v0.7.0 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect github.com/smallstep/truststore v0.13.0 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.8.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect - github.com/urfave/cli v1.22.14 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect - go.etcd.io/bbolt v1.3.9 // indirect - go.step.sm/cli-utils v0.9.0 // indirect - go.step.sm/crypto v0.45.0 // indirect - go.step.sm/linkedca v0.20.1 // indirect - go.uber.org/automaxprocs v1.5.3 // indirect - go.uber.org/mock v0.4.0 // indirect + github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect + github.com/urfave/cli v1.22.17 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.step.sm/crypto v0.67.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.22.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.1 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/api v0.240.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect howett.net/plist v1.0.0 // indirect + modernc.org/libc v1.65.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 966bc61..93a65c5 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,35 @@ -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= -cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY= -cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= +github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -36,47 +40,48 @@ github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= -github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A= -github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= +github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= +github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= -github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw= -github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= -github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= +github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8= +github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0= +github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0= +github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= +github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -86,18 +91,17 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -107,8 +111,8 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -117,38 +121,20 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -157,149 +143,94 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= -github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= -github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= -github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= +github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= +github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k= -github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= -github.com/mholt/caddy-l4 v0.0.0-20241104153248-ec8fae209322 h1:2cH9FoIRmAOe7XGxUEBJvcu3RckQYX5uk9YyPJONsCU= -github.com/mholt/caddy-l4 v0.0.0-20241104153248-ec8fae209322/go.mod h1:zhoEExOYPSuKYLyJE88BOIHNNf3PdOLyYEYbtnmgcSw= -github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/caddy-l4 v0.0.0-20251204151317-049ea4dcfaf0 h1:QtoAgPSJe1NwdqHmhyQmCp9PHOOMQTrybrie6qXcfzg= +github.com/mholt/caddy-l4 v0.0.0-20251204151317-049ea4dcfaf0/go.mod h1:QE3J4K4lDelBdkhqeybY6DnvhdiV6K+443fcFzymQyU= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= +github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= @@ -310,57 +241,55 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= -github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slackhq/nebula v1.7.2 h1:Rko1Mlksz/nC0c919xjGpB8uOSrTJ5e6KPgZx+lVfYw= -github.com/slackhq/nebula v1.7.2/go.mod h1:cnaoahkUipDs1vrNoIszyp0QPRIQN9Pm68ppQEW1Fhg= +github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= +github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o= -github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis= -github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= -github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y= -github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y= -github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg= -github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= -github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= -github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU= +github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= +github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA= +github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= +github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= +github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= +github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= +github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= +github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y= github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -368,233 +297,227 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU= -github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= -go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= -go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= -go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc= -go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY= -go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU= -go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= +go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= -google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= +modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= +modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/l4handler.go b/l4handler.go index 77833e5..e22bc39 100644 --- a/l4handler.go +++ b/l4handler.go @@ -2,6 +2,7 @@ package sipguardian import ( "bytes" + "fmt" "io" "net" "regexp" @@ -48,17 +49,21 @@ func (m *SIPMatcher) Provision(ctx caddy.Context) error { // Match returns true if the connection appears to be SIP traffic func (m *SIPMatcher) Match(cx *layer4.Connection) (bool, error) { - // Peek at first 64 bytes to check for SIP signature + // Read enough bytes to identify SIP traffic + // We need at least 8 bytes to identify SIP methods (e.g., "REGISTER " or "SIP/2.0 ") buf := make([]byte, 64) - n, err := io.ReadAtLeast(cx, buf, 8) - if err != nil && err != io.ErrUnexpectedEOF { - return false, nil + n, err := io.ReadFull(cx, buf) + if err == io.ErrUnexpectedEOF && n >= 8 { + // Got less than 64 bytes but enough to match - that's fine + buf = buf[:n] + } else if err != nil { + // Return the error so caddy-l4 knows we need more data + // This includes ErrConsumedAllPrefetchedBytes which triggers prefetch + return false, err } - buf = buf[:n] - // Check if it matches a SIP method + // Check if it matches a SIP method (REGISTER, INVITE, OPTIONS, etc.) if m.methodRegex.Match(buf) { - // Rewind the buffer for the handler cx.SetVar("sip_peek", buf) return true, nil } @@ -80,6 +85,10 @@ type SIPHandler struct { // Upstream address to proxy to Upstream string `json:"upstream,omitempty"` + // Embedded guardian config parsed from Caddyfile + // This gets applied to the shared guardian during Provision + SIPGuardian + logger *zap.Logger guardian *SIPGuardian } @@ -94,13 +103,13 @@ func (SIPHandler) CaddyModule() caddy.ModuleInfo { func (h *SIPHandler) Provision(ctx caddy.Context) error { h.logger = ctx.Logger() - // Get or create the guardian instance - // In a real implementation, this would use Caddy's module loading - // For now, we'll create a default instance - h.guardian = &SIPGuardian{} - if err := h.guardian.Provision(ctx); err != nil { + // Get or create a shared guardian instance from the global registry + // Pass our parsed config so the guardian can be configured + guardian, err := GetOrCreateGuardianWithConfig(ctx, "default", &h.SIPGuardian) + if err != nil { return err } + h.guardian = guardian return nil } @@ -116,59 +125,185 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error { // Check if IP is banned if h.guardian.IsBanned(host) { h.logger.Debug("Blocked banned IP", zap.String("ip", host)) + if enableMetrics { + RecordConnection("blocked") + } return cx.Close() } // Check if IP is whitelisted - skip further checks if h.guardian.IsWhitelisted(host) { + if enableMetrics { + RecordConnection("allowed") + } return next.Handle(cx) } - // Get the peeked SIP data if available - if peekData := cx.GetVar("sip_peek"); peekData != nil { - buf := peekData.([]byte) + // Check GeoIP blocking (if configured) + if blocked, country := h.guardian.IsCountryBlocked(host); blocked { + h.logger.Info("Blocked connection from blocked country", + zap.String("ip", host), + zap.String("country", country), + ) + if enableMetrics { + RecordConnection("geo_blocked") + } + return cx.Close() + } - // Check for suspicious patterns - if isSuspiciousSIP(buf) { - h.logger.Warn("Suspicious SIP traffic detected", - zap.String("ip", host), - zap.ByteString("sample", buf[:min(32, len(buf))]), - ) - banned := h.guardian.RecordFailure(host, "suspicious_sip_pattern") - if banned { + // Read data from the connection for suspicious pattern detection + // caddy-l4 replays prefetched data on read, so we can read the full message here + buf := make([]byte, 1024) + n, err := cx.Read(buf) + if n > 0 { + buf = buf[:n] + h.logger.Debug("Read SIP data for inspection", + zap.String("ip", host), + zap.Int("bytes", n), + ) + + // Extract SIP method for rate limiting + method := ExtractSIPMethod(buf) + if method != "" { + // Check rate limit + rl := GetRateLimiter(h.logger) + if allowed, reason := rl.Allow(host, method); !allowed { + h.logger.Warn("Rate limit exceeded", + zap.String("ip", host), + zap.String("method", string(method)), + ) + if enableMetrics { + RecordConnection("rate_limited") + } + // Record as failure (may trigger ban) + h.guardian.RecordFailure(host, reason) return cx.Close() } } + + // Check for extension enumeration attacks + extension := ExtractTargetExtension(buf) + if extension != "" { + detector := GetEnumerationDetector(h.logger) + result := detector.RecordAttempt(host, extension) + if result.Detected { + h.logger.Warn("Enumeration attack detected", + zap.String("ip", host), + zap.String("reason", result.Reason), + zap.Int("unique_extensions", result.UniqueCount), + zap.Strings("extensions", result.Extensions), + ) + + if enableMetrics { + RecordEnumerationDetection(result.Reason) + RecordEnumerationExtensions(result.UniqueCount) + RecordConnection("enumeration_blocked") + } + + // Store in persistent storage if enabled + if h.guardian.storage != nil { + go h.guardian.storage.RecordEnumerationAttempt(host, result.Reason, result.UniqueCount, result.Extensions) + } + + // Emit webhook event + if enableWebhooks { + go EmitEnumerationEvent(h.logger, host, result) + } + + // Ban the IP (use enumeration-specific ban time if configured) + h.guardian.RecordFailure(host, "enumeration_"+result.Reason) + return cx.Close() + } + + // Update metrics for tracked IPs + if enableMetrics { + stats := detector.GetStats() + if trackedIPs, ok := stats["tracked_ips"].(int); ok { + UpdateEnumerationTrackedIPs(trackedIPs) + } + } + } + + // Check for suspicious patterns in the SIP message + suspiciousPattern := detectSuspiciousPattern(buf) + if suspiciousPattern != "" { + h.logger.Warn("Suspicious SIP traffic detected", + zap.String("ip", host), + zap.String("pattern", suspiciousPattern), + zap.ByteString("sample", buf[:min(64, len(buf))]), + ) + if enableMetrics { + RecordSuspiciousPattern(suspiciousPattern) + RecordConnection("suspicious") + } + + // Store in persistent storage if enabled + if h.guardian.storage != nil { + go h.guardian.storage.RecordSuspiciousPattern(host, suspiciousPattern, string(buf[:min(200, len(buf))])) + } + + banned := h.guardian.RecordFailure(host, "suspicious_sip_pattern") + if banned { + h.logger.Warn("IP banned due to suspicious activity", + zap.String("ip", host), + ) + return cx.Close() + } + } + } else if err != nil { + h.logger.Debug("Failed to read SIP data for inspection", + zap.String("ip", host), + zap.Error(err), + ) + } + + // Record successful connection + if enableMetrics { + RecordConnection("allowed") } // Continue to next handler return next.Handle(cx) } -// isSuspiciousSIP checks for common attack patterns in SIP traffic -func isSuspiciousSIP(data []byte) bool { - s := string(data) +// suspiciousPatternDefs defines patterns and their names for detection +var suspiciousPatternDefs = []struct { + name string + pattern string +}{ + {"sipvicious", "sipvicious"}, + {"friendly-scanner", "friendly-scanner"}, + {"sipcli", "sipcli"}, + {"sip-scan", "sip-scan"}, + {"voipbuster", "voipbuster"}, + {"asterisk-pbx-scanner", "asterisk pbx"}, + {"sipsak", "sipsak"}, + {"sundayddr", "sundayddr"}, + {"iwar", "iwar"}, + {"cseq-flood", "cseq: 1 options"}, // Repeated OPTIONS flood + {"zoiper-spoof", "user-agent: zoiper"}, + {"test-extension-100", "sip:100@"}, + {"test-extension-1000", "sip:1000@"}, + {"null-user", "sip:@"}, + {"anonymous", "anonymous@"}, +} - // Common scanner/attack patterns - suspiciousPatterns := []string{ - "sipvicious", - "friendly-scanner", - "sipcli", - "sip-scan", - "User-Agent: Zoiper", // Often spoofed - "From: tb.maxTokens { + tb.tokens = tb.maxTokens + } + tb.lastUpdate = now + + // Check if we have tokens + if tb.tokens >= 1.0 { + tb.tokens -= 1.0 + return true, "" + } + + // Rate limited + return false, "rate_limit_" + string(method) +} + +// Cleanup removes old entries +func (rl *RateLimiter) Cleanup() { + rl.mu.Lock() + defer rl.mu.Unlock() + + now := time.Now() + cutoff := now.Add(-10 * time.Minute) + + for ip, bucket := range rl.buckets { + bucket.mu.Lock() + if bucket.lastReset.Before(cutoff) { + delete(rl.buckets, ip) + } + bucket.mu.Unlock() + } +} + +// GetStats returns rate limiter statistics +func (rl *RateLimiter) GetStats() map[string]interface{} { + rl.mu.RLock() + defer rl.mu.RUnlock() + + limitConfigs := make(map[string]interface{}) + for method, limit := range rl.limits { + limitConfigs[string(method)] = map[string]interface{}{ + "max_requests": limit.MaxRequests, + "window": limit.Window.String(), + "burst_size": limit.BurstSize, + } + } + + return map[string]interface{}{ + "tracked_ips": len(rl.buckets), + "limits": limitConfigs, + } +} + +// SIP method extraction patterns +var ( + sipMethodPattern = regexp.MustCompile(`^(REGISTER|INVITE|OPTIONS|ACK|BYE|CANCEL|INFO|NOTIFY|SUBSCRIBE|MESSAGE|UPDATE|PRACK|REFER|PUBLISH)\s+sip:`) + // Pattern to extract target extension from Request-URI: METHOD sip:extension@domain + sipTargetExtPattern = regexp.MustCompile(`^(?:REGISTER|INVITE|OPTIONS|MESSAGE|SUBSCRIBE)\s+sip:([^@\s>]+)@`) +) + +// ExtractSIPMethod extracts the SIP method from a message +func ExtractSIPMethod(data []byte) SIPMethod { + s := string(data) + if matches := sipMethodPattern.FindStringSubmatch(s); len(matches) > 1 { + return SIPMethod(matches[1]) + } + return "" +} + +// ExtractTargetExtension extracts the target extension from a SIP message +// It looks at the Request-URI first, then falls back to the To header +func ExtractTargetExtension(data []byte) string { + s := string(data) + + // First try Request-URI: REGISTER sip:1001@domain.com + if matches := sipTargetExtPattern.FindStringSubmatch(s); len(matches) > 1 { + ext := matches[1] + // Filter out domain-like values and overly long strings + if !strings.Contains(ext, ".") && len(ext) <= 10 && len(ext) > 0 { + return ext + } + } + + // Fall back to To header + user, _ := ParseToHeader(data) + if user != "" && !strings.Contains(user, ".") && len(user) <= 10 { + return user + } + + return "" +} + +// ParseUserAgent extracts User-Agent from SIP message +func ParseUserAgent(data []byte) string { + s := string(data) + lines := strings.Split(s, "\r\n") + for _, line := range lines { + lower := strings.ToLower(line) + if strings.HasPrefix(lower, "user-agent:") { + return strings.TrimSpace(line[11:]) + } + } + return "" +} + +// ParseFromHeader extracts From header info +func ParseFromHeader(data []byte) (user, domain string) { + s := string(data) + lines := strings.Split(s, "\r\n") + for _, line := range lines { + lower := strings.ToLower(line) + if strings.HasPrefix(lower, "from:") { + // Extract sip:user@domain from From header + fromPattern := regexp.MustCompile(`sip:([^@]+)@([^>;\s]+)`) + if matches := fromPattern.FindStringSubmatch(line); len(matches) > 2 { + return matches[1], matches[2] + } + } + } + return "", "" +} + +// ParseToHeader extracts To header info +func ParseToHeader(data []byte) (user, domain string) { + s := string(data) + lines := strings.Split(s, "\r\n") + for _, line := range lines { + lower := strings.ToLower(line) + if strings.HasPrefix(lower, "to:") { + // Extract sip:user@domain from To header + toPattern := regexp.MustCompile(`sip:([^@]+)@([^>;\s]+)`) + if matches := toPattern.FindStringSubmatch(line); len(matches) > 2 { + return matches[1], matches[2] + } + } + } + return "", "" +} + +// ParseCallID extracts Call-ID from SIP message +func ParseCallID(data []byte) string { + s := string(data) + lines := strings.Split(s, "\r\n") + for _, line := range lines { + lower := strings.ToLower(line) + if strings.HasPrefix(lower, "call-id:") { + return strings.TrimSpace(line[8:]) + } + if strings.HasPrefix(lower, "i:") { // Short form + return strings.TrimSpace(line[2:]) + } + } + return "" +} diff --git a/registry.go b/registry.go index ad7bd50..47912eb 100644 --- a/registry.go +++ b/registry.go @@ -1,9 +1,12 @@ package sipguardian import ( + "net" "sync" + "time" "github.com/caddyserver/caddy/v2" + "go.uber.org/zap" ) // Global registry to share guardian instances across modules @@ -12,8 +15,13 @@ var ( registryMu sync.RWMutex ) -// GetOrCreateGuardian returns a shared guardian instance by name +// GetOrCreateGuardian returns a shared guardian instance by name (backward compat) func GetOrCreateGuardian(ctx caddy.Context, name string) (*SIPGuardian, error) { + return GetOrCreateGuardianWithConfig(ctx, name, nil) +} + +// GetOrCreateGuardianWithConfig returns a shared guardian instance, merging config if provided +func GetOrCreateGuardianWithConfig(ctx caddy.Context, name string, config *SIPGuardian) (*SIPGuardian, error) { if name == "" { name = "default" } @@ -22,11 +30,33 @@ func GetOrCreateGuardian(ctx caddy.Context, name string) (*SIPGuardian, error) { defer registryMu.Unlock() if g, exists := guardianRegistry[name]; exists { + // Guardian exists - merge any new config + if config != nil { + mergeGuardianConfig(ctx, g, config) + } return g, nil } - // Create new guardian - g := &SIPGuardian{} + // Create new guardian with config + var g *SIPGuardian + if config != nil { + // Copy config values to a new guardian + g = &SIPGuardian{ + MaxFailures: config.MaxFailures, + FindTime: config.FindTime, + BanTime: config.BanTime, + WhitelistCIDR: config.WhitelistCIDR, + Webhooks: config.Webhooks, + StoragePath: config.StoragePath, + GeoIPPath: config.GeoIPPath, + BlockedCountries: config.BlockedCountries, + AllowedCountries: config.AllowedCountries, + Enumeration: config.Enumeration, + } + } else { + g = &SIPGuardian{} + } + if err := g.Provision(ctx); err != nil { return nil, err } @@ -35,6 +65,142 @@ func GetOrCreateGuardian(ctx caddy.Context, name string) (*SIPGuardian, error) { return g, nil } +// mergeGuardianConfig merges new config into an existing guardian +// This handles cases where multiple handlers might specify overlapping config +func mergeGuardianConfig(ctx caddy.Context, g *SIPGuardian, config *SIPGuardian) { + g.mu.Lock() + defer g.mu.Unlock() + + logger := ctx.Logger() + + // Merge whitelist CIDRs (add new ones, avoid duplicates) + for _, cidr := range config.WhitelistCIDR { + found := false + for _, existing := range g.WhitelistCIDR { + if existing == cidr { + found = true + break + } + } + if !found { + g.WhitelistCIDR = append(g.WhitelistCIDR, cidr) + // Parse and add to whitelistNets + if _, network, err := net.ParseCIDR(cidr); err == nil { + g.whitelistNets = append(g.whitelistNets, network) + logger.Debug("Added whitelist CIDR from handler config", + zap.String("cidr", cidr), + ) + } + } + } + + // Override numeric values if they're non-zero (handler specified them) + if config.MaxFailures > 0 && config.MaxFailures != g.MaxFailures { + g.MaxFailures = config.MaxFailures + } + if config.FindTime > 0 && config.FindTime != g.FindTime { + g.FindTime = config.FindTime + } + if config.BanTime > 0 && config.BanTime != g.BanTime { + g.BanTime = config.BanTime + } + + // Initialize storage if specified and not yet initialized + if config.StoragePath != "" && g.storage == nil { + storage, err := InitStorage(logger, StorageConfig{ + Path: config.StoragePath, + }) + if err != nil { + logger.Warn("Failed to initialize storage from handler config", + zap.Error(err), + ) + } else { + g.storage = storage + g.StoragePath = config.StoragePath + // Load existing bans from storage + if bans, err := storage.LoadActiveBans(); err == nil { + for _, ban := range bans { + entry := ban + g.bannedIPs[entry.IP] = &entry + } + logger.Info("Loaded bans from storage", zap.Int("count", len(bans))) + } + } + } + + // Initialize GeoIP if specified and not yet initialized + if config.GeoIPPath != "" && g.geoIP == nil { + geoIP, err := NewGeoIPLookup(config.GeoIPPath) + if err != nil { + logger.Warn("Failed to initialize GeoIP from handler config", + zap.Error(err), + ) + } else { + g.geoIP = geoIP + g.GeoIPPath = config.GeoIPPath + } + } + + // Merge blocked/allowed countries + for _, country := range config.BlockedCountries { + found := false + for _, existing := range g.BlockedCountries { + if existing == country { + found = true + break + } + } + if !found { + g.BlockedCountries = append(g.BlockedCountries, country) + } + } + for _, country := range config.AllowedCountries { + found := false + for _, existing := range g.AllowedCountries { + if existing == country { + found = true + break + } + } + if !found { + g.AllowedCountries = append(g.AllowedCountries, country) + } + } + + // Merge webhooks (add new ones by URL) + for _, webhook := range config.Webhooks { + found := false + for _, existing := range g.Webhooks { + if existing.URL == webhook.URL { + found = true + break + } + } + if !found { + g.Webhooks = append(g.Webhooks, webhook) + // Register with webhook manager + if enableWebhooks { + wm := GetWebhookManager(logger) + wm.AddWebhook(webhook) + } + } + } + + // Apply enumeration config if specified + if config.Enumeration != nil && g.Enumeration == nil { + g.Enumeration = config.Enumeration + // Apply to global detector + SetEnumerationConfig(*config.Enumeration) + logger.Debug("Applied enumeration config from handler") + } + + logger.Debug("Merged guardian config", + zap.Int("whitelist_count", len(g.whitelistNets)), + zap.Int("webhook_count", len(g.Webhooks)), + zap.Duration("ban_time", time.Duration(g.BanTime)), + ) +} + // GetGuardian returns an existing guardian instance func GetGuardian(name string) *SIPGuardian { if name == "" { diff --git a/sandbox/Caddyfile b/sandbox/Caddyfile new file mode 100644 index 0000000..960d137 --- /dev/null +++ b/sandbox/Caddyfile @@ -0,0 +1,107 @@ +# Sandbox Caddyfile for SIP Guardian Testing +# +# This configuration showcases all the new features: +# - Prometheus metrics endpoint +# - Rate limiting per method (built-in defaults) +# - Suspicious pattern detection +# +# Note: Storage and webhooks are configured in JSON config mode, +# as the L4 handler uses the shared global guardian instance + +{ + debug + + admin 0.0.0.0:2019 + + layer4 { + # SIP over UDP + udp/:5060 { + @sip sip { + methods REGISTER INVITE OPTIONS ACK BYE CANCEL INFO NOTIFY SUBSCRIBE MESSAGE + } + + route @sip { + sip_guardian { + max_failures 3 # Lower for faster testing + find_time 2m # Shorter window + ban_time 5m # Short bans for testing + + # Whitelist legitimate test clients + whitelist 10.55.0.50/32 # client container + whitelist 10.55.0.51/32 # linphone container + + # Enumeration detection (low thresholds for testing) + enumeration { + max_extensions 10 + extension_window 2m + sequential_threshold 5 + rapid_fire_count 8 + rapid_fire_window 10s + ban_time 10m + exempt_extensions 100 200 + } + } + proxy udp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_PORT} + } + + # Unmatched traffic - drop silently + route { + } + } + + # SIP over TCP + tcp/:5060 { + @sip sip + + route @sip { + sip_guardian { + max_failures 3 + find_time 2m + ban_time 5m + whitelist 10.55.0.50/32 + whitelist 10.55.0.51/32 + } + proxy tcp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_PORT} + } + } + + # SIP over TLS + tcp/:5061 { + @sip sip + + route @sip { + sip_guardian { + max_failures 3 + find_time 2m + ban_time 5m + whitelist 10.55.0.50/32 + whitelist 10.55.0.51/32 + } + proxy tcp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_TLS_PORT} + } + } + } +} + +# Admin API and Metrics +:2020 { + # SIP Guardian admin endpoints + handle /api/sip-guardian/* { + sip_guardian_admin + } + + # Prometheus metrics endpoint + handle /metrics { + sip_guardian_metrics + } + + # Health check + handle /health { + respond "OK" 200 + } + + # Stats (alias for backwards compatibility) + handle /stats { + sip_guardian_admin + } +} diff --git a/sandbox/docker-compose.yml b/sandbox/docker-compose.yml new file mode 100644 index 0000000..16fc3cc --- /dev/null +++ b/sandbox/docker-compose.yml @@ -0,0 +1,219 @@ +# SIP Guardian Testing Sandbox +# +# This provides a complete testing environment with: +# - FreePBX (real PBX for testing) +# - Caddy with SIP Guardian (the proxy under test) +# - Attack containers (sipvicious, custom scripts) +# - Valid client containers (pjsip for legitimate traffic) +# +# Usage: +# docker compose up -d +# docker compose logs -f caddy +# docker compose run --rm attacker sipvicious_svwar -e100-200 caddy +# docker compose run --rm client pjsua --registrar=sip:caddy + +services: + # ============================================ + # FreePBX - The Protected PBX + # ============================================ + freepbx: + image: tiredofit/freepbx:latest + container_name: sandbox-freepbx + hostname: freepbx + restart: unless-stopped + privileged: true + environment: + - VIRTUAL_HOST=pbx.sandbox.local + - VIRTUAL_NETWORK=sandbox + - HTTP_PORT=80 + - HTTPS_PORT=443 + - UCP_FIRST_RUN=true + - DB_HOST=mariadb + - DB_PORT=3306 + - DB_NAME=asterisk + - DB_USER=asterisk + - DB_PASS=asteriskpass + - ENABLE_FAIL2BAN=FALSE # We're replacing this! + - TZ=UTC + volumes: + - freepbx_data:/data + - freepbx_logs:/var/log + - freepbx_www:/var/www/html + depends_on: + - mariadb + networks: + sandbox: + ipv4_address: 10.55.0.10 + + mariadb: + image: mariadb:10.11 + container_name: sandbox-mariadb + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=rootpass + - MYSQL_DATABASE=asterisk + - MYSQL_USER=asterisk + - MYSQL_PASSWORD=asteriskpass + volumes: + - mariadb_data:/var/lib/mysql + networks: + sandbox: + ipv4_address: 10.55.0.11 + + # ============================================ + # Caddy with SIP Guardian - The Proxy + # ============================================ + caddy: + build: + context: .. + dockerfile: Dockerfile + container_name: sandbox-caddy + hostname: caddy + restart: unless-stopped + # Override default command to use explicit Caddyfile (ENTRYPOINT is "caddy") + command: ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] + ports: + # Expose SIP ports to host for external testing + - "5060:5060/udp" + - "5060:5060/tcp" + - "5061:5061/tcp" + # Admin API + - "2020:2020" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + environment: + - SIP_UPSTREAM_HOST=freepbx + - SIP_UPSTREAM_PORT=5060 + - SIP_UPSTREAM_TLS_PORT=5061 + networks: + sandbox: + ipv4_address: 10.55.0.2 + depends_on: + - freepbx + + # ============================================ + # Attack Simulation Containers + # ============================================ + + # SIPVicious scanner - for testing scanner detection + attacker: + image: python:3.11-slim + container_name: sandbox-attacker + hostname: attacker + profiles: + - testing + command: > + bash -c " + pip install sipvicious pysip3 && + echo 'SIPVicious and tools installed. Use: sipvicious_svwar, sipvicious_svcrack, sipvicious_svmap' && + tail -f /dev/null + " + networks: + sandbox: + ipv4_address: 10.55.0.100 + depends_on: + - caddy + + # Brute force simulation + bruteforcer: + image: python:3.11-slim + container_name: sandbox-bruteforcer + hostname: bruteforcer + profiles: + - testing + volumes: + - ./scripts:/scripts:ro + command: > + bash -c " + pip install sipsimple requests && + echo 'Brute force tools ready. Run: python /scripts/bruteforce.py' && + tail -f /dev/null + " + networks: + sandbox: + ipv4_address: 10.55.0.101 + depends_on: + - caddy + + # ============================================ + # Legitimate Client Containers + # ============================================ + + # PJSIP-based SIP client + client: + image: alpine:latest + container_name: sandbox-client + hostname: client + profiles: + - testing + command: > + sh -c " + apk add --no-cache pjsua netcat-openbsd curl jq && + echo 'PJSIP client ready. Use pjsua for SIP registration.' && + tail -f /dev/null + " + networks: + sandbox: + ipv4_address: 10.55.0.50 + depends_on: + - caddy + + # Linphone CLI client + linphone: + image: alpine:latest + container_name: sandbox-linphone + hostname: linphone + profiles: + - testing + command: > + sh -c " + apk add --no-cache linphone netcat-openbsd curl && + echo 'Linphone ready.' && + tail -f /dev/null + " + networks: + sandbox: + ipv4_address: 10.55.0.51 + depends_on: + - caddy + + # ============================================ + # Monitoring & Debugging + # ============================================ + + # Network sniffer for SIP traffic analysis + tcpdump: + image: alpine:latest + container_name: sandbox-tcpdump + hostname: tcpdump + profiles: + - debug + cap_add: + - NET_ADMIN + - NET_RAW + network_mode: "service:caddy" + command: > + sh -c " + apk add --no-cache tcpdump && + tcpdump -i any -n 'udp port 5060 or tcp port 5060 or tcp port 5061' -vvv + " + depends_on: + - caddy + +volumes: + freepbx_data: + freepbx_logs: + freepbx_www: + mariadb_data: + caddy_data: + caddy_config: + +networks: + sandbox: + driver: bridge + ipam: + config: + - subnet: 10.55.0.0/24 + gateway: 10.55.0.1 diff --git a/sandbox/scripts/bruteforce.py b/sandbox/scripts/bruteforce.py new file mode 100644 index 0000000..ced6873 --- /dev/null +++ b/sandbox/scripts/bruteforce.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +SIP Brute Force Simulation for SIP Guardian Testing + +Simulates authentication failures to test rate limiting and banning. +""" + +import socket +import time +import argparse +import random +import string + +def generate_call_id(): + """Generate a random SIP Call-ID""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + +def generate_branch(): + """Generate a random Via branch parameter""" + return 'z9hG4bK' + ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + +def create_register_request(target_host: str, target_port: int, extension: str, from_ip: str) -> bytes: + """Create a SIP REGISTER request""" + call_id = generate_call_id() + branch = generate_branch() + tag = ''.join(random.choices(string.digits, k=8)) + + request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r +Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r +Max-Forwards: 70\r +From: ;tag={tag}\r +To: \r +Call-ID: {call_id}@{from_ip}\r +CSeq: 1 REGISTER\r +Contact: \r +Expires: 3600\r +User-Agent: BruteForcer/1.0\r +Content-Length: 0\r +\r +""" + return request.encode() + +def send_register(sock: socket.socket, target: tuple, request: bytes) -> str: + """Send REGISTER and receive response""" + try: + sock.sendto(request, target) + sock.settimeout(2.0) + response, _ = sock.recvfrom(4096) + return response.decode() + except socket.timeout: + return "TIMEOUT" + except Exception as e: + return f"ERROR: {e}" + +def main(): + parser = argparse.ArgumentParser(description='SIP Brute Force Simulator') + parser.add_argument('target', help='Target host (Caddy proxy)') + parser.add_argument('-p', '--port', type=int, default=5060, help='Target port') + parser.add_argument('-e', '--extensions', default='100-105', help='Extension range (e.g., 100-200)') + parser.add_argument('-c', '--count', type=int, default=10, help='Attempts per extension') + parser.add_argument('-d', '--delay', type=float, default=0.1, help='Delay between attempts') + parser.add_argument('--udp', action='store_true', default=True, help='Use UDP (default)') + parser.add_argument('--tcp', action='store_true', help='Use TCP') + args = parser.parse_args() + + # Parse extension range + if '-' in args.extensions: + start, end = map(int, args.extensions.split('-')) + extensions = list(range(start, end + 1)) + else: + extensions = [int(args.extensions)] + + # Get our IP + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect((args.target, args.port)) + from_ip = sock.getsockname()[0] + sock.close() + + print(f"[*] Starting brute force simulation") + print(f"[*] Target: {args.target}:{args.port}") + print(f"[*] Source IP: {from_ip}") + print(f"[*] Extensions: {extensions}") + print(f"[*] Attempts per extension: {args.count}") + print() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + target = (args.target, args.port) + + attempts = 0 + blocked_at = None + + for ext in extensions: + for i in range(args.count): + request = create_register_request(args.target, args.port, str(ext), from_ip) + response = send_register(sock, target, request) + attempts += 1 + + if 'TIMEOUT' in response: + if blocked_at is None: + blocked_at = attempts + print(f"[!] BLOCKED after {attempts} attempts (extension {ext}, attempt {i+1})") + print(f"[+] SIP Guardian is working! Blocked after {blocked_at} attempts") + return + elif '401' in response or '407' in response: + print(f"[*] Auth required: ext={ext} attempt={i+1} total={attempts}") + elif '403' in response: + print(f"[!] FORBIDDEN: ext={ext} - Connection blocked") + if blocked_at is None: + blocked_at = attempts + else: + # Print first line of response + first_line = response.split('\r\n')[0] if response else 'No response' + print(f"[?] Response: {first_line} (ext={ext})") + + time.sleep(args.delay) + + print(f"\n[*] Completed {attempts} attempts without being blocked") + print("[!] SIP Guardian may not be working correctly") + + sock.close() + +if __name__ == '__main__': + main() diff --git a/sandbox/scripts/valid_register.py b/sandbox/scripts/valid_register.py new file mode 100644 index 0000000..f95cd9b --- /dev/null +++ b/sandbox/scripts/valid_register.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Valid SIP Registration Test + +Tests that legitimate registrations pass through SIP Guardian successfully. +""" + +import socket +import time +import argparse +import random +import string +import hashlib + +def generate_call_id(): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + +def generate_branch(): + return 'z9hG4bK' + ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + +def compute_digest_response(username: str, password: str, realm: str, nonce: str, uri: str, method: str = 'REGISTER') -> str: + """Compute SIP Digest authentication response""" + ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest() + ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest() + response = hashlib.md5(f"{ha1}:{nonce}:{ha2}".encode()).hexdigest() + return response + +def create_register_with_auth(target_host: str, target_port: int, extension: str, + password: str, from_ip: str, realm: str, nonce: str) -> bytes: + """Create an authenticated SIP REGISTER request""" + call_id = generate_call_id() + branch = generate_branch() + tag = ''.join(random.choices(string.digits, k=8)) + uri = f"sip:{target_host}:{target_port}" + + response = compute_digest_response(extension, password, realm, nonce, uri) + + request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r +Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r +Max-Forwards: 70\r +From: ;tag={tag}\r +To: \r +Call-ID: {call_id}@{from_ip}\r +CSeq: 2 REGISTER\r +Contact: \r +Authorization: Digest username="{extension}",realm="{realm}",nonce="{nonce}",uri="{uri}",response="{response}",algorithm=MD5\r +Expires: 3600\r +User-Agent: ValidClient/1.0\r +Content-Length: 0\r +\r +""" + return request.encode() + +def create_initial_register(target_host: str, target_port: int, extension: str, from_ip: str) -> bytes: + """Create initial REGISTER without auth (to get challenge)""" + call_id = generate_call_id() + branch = generate_branch() + tag = ''.join(random.choices(string.digits, k=8)) + + request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r +Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r +Max-Forwards: 70\r +From: ;tag={tag}\r +To: \r +Call-ID: {call_id}@{from_ip}\r +CSeq: 1 REGISTER\r +Contact: \r +Expires: 3600\r +User-Agent: ValidClient/1.0\r +Content-Length: 0\r +\r +""" + return request.encode() + +def parse_www_authenticate(response: str) -> tuple: + """Parse WWW-Authenticate header to get realm and nonce""" + for line in response.split('\r\n'): + if line.lower().startswith('www-authenticate:'): + # Extract realm + realm_start = line.find('realm="') + 7 + realm_end = line.find('"', realm_start) + realm = line[realm_start:realm_end] if realm_start > 6 else '' + + # Extract nonce + nonce_start = line.find('nonce="') + 7 + nonce_end = line.find('"', nonce_start) + nonce = line[nonce_start:nonce_end] if nonce_start > 6 else '' + + return realm, nonce + return '', '' + +def main(): + parser = argparse.ArgumentParser(description='Valid SIP Registration Test') + parser.add_argument('target', help='Target host (Caddy proxy)') + parser.add_argument('-p', '--port', type=int, default=5060, help='Target port') + parser.add_argument('-e', '--extension', default='100', help='Extension to register') + parser.add_argument('-s', '--secret', default='password123', help='Extension password') + parser.add_argument('-r', '--repeat', type=int, default=1, help='Number of registration cycles') + args = parser.parse_args() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect((args.target, args.port)) + from_ip = sock.getsockname()[0] + sock.close() + + print(f"[*] Valid Registration Test") + print(f"[*] Target: {args.target}:{args.port}") + print(f"[*] Extension: {args.extension}") + print(f"[*] Source IP: {from_ip}") + print() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + target = (args.target, args.port) + + for cycle in range(args.repeat): + print(f"[*] Registration cycle {cycle + 1}/{args.repeat}") + + # Send initial REGISTER + request = create_initial_register(args.target, args.port, args.extension, from_ip) + sock.sendto(request, target) + + try: + sock.settimeout(5.0) + response, _ = sock.recvfrom(4096) + response = response.decode() + except socket.timeout: + print("[!] TIMEOUT - Connection may be blocked") + continue + + first_line = response.split('\r\n')[0] + print(f"[*] Initial response: {first_line}") + + if '401' in response or '407' in response: + # Parse auth challenge + realm, nonce = parse_www_authenticate(response) + print(f"[*] Got challenge: realm={realm}") + + # Send authenticated REGISTER + auth_request = create_register_with_auth( + args.target, args.port, args.extension, args.secret, + from_ip, realm, nonce + ) + sock.sendto(auth_request, target) + + try: + response, _ = sock.recvfrom(4096) + response = response.decode() + first_line = response.split('\r\n')[0] + print(f"[*] Auth response: {first_line}") + + if '200' in response: + print("[+] SUCCESS - Registration completed!") + else: + print(f"[!] Failed: {first_line}") + except socket.timeout: + print("[!] TIMEOUT after auth - may be blocked") + elif '200' in response: + print("[+] SUCCESS - Already registered or no auth required") + else: + print(f"[?] Unexpected response: {first_line}") + + if cycle < args.repeat - 1: + time.sleep(2) + + sock.close() + +if __name__ == '__main__': + main() diff --git a/sipguardian.go b/sipguardian.go index cac888a..60e80c2 100644 --- a/sipguardian.go +++ b/sipguardian.go @@ -13,6 +13,13 @@ import ( "go.uber.org/zap" ) +// Feature flags for optional components +var ( + enableMetrics = true + enableWebhooks = true + enableStorage = true +) + func init() { caddy.RegisterModule(SIPGuardian{}) } @@ -29,17 +36,33 @@ type BanEntry struct { // SIPGuardian implements intelligent SIP protection at Layer 4 type SIPGuardian struct { // Configuration - MaxFailures int `json:"max_failures,omitempty"` + MaxFailures int `json:"max_failures,omitempty"` FindTime caddy.Duration `json:"find_time,omitempty"` BanTime caddy.Duration `json:"ban_time,omitempty"` WhitelistCIDR []string `json:"whitelist_cidr,omitempty"` + // Webhook configuration + Webhooks []WebhookConfig `json:"webhooks,omitempty"` + + // Storage configuration + StoragePath string `json:"storage_path,omitempty"` + + // GeoIP configuration + GeoIPPath string `json:"geoip_path,omitempty"` + BlockedCountries []string `json:"blocked_countries,omitempty"` + AllowedCountries []string `json:"allowed_countries,omitempty"` + + // Enumeration detection configuration + Enumeration *EnumerationConfig `json:"enumeration,omitempty"` + // Runtime state - logger *zap.Logger - bannedIPs map[string]*BanEntry + logger *zap.Logger + bannedIPs map[string]*BanEntry failureCounts map[string]*failureTracker whitelistNets []*net.IPNet - mu sync.RWMutex + mu sync.RWMutex + storage *Storage + geoIP *GeoIPLookup } type failureTracker struct { @@ -82,6 +105,63 @@ func (g *SIPGuardian) Provision(ctx caddy.Context) error { g.whitelistNets = append(g.whitelistNets, network) } + // Initialize metrics + if enableMetrics { + RegisterMetrics() + } + + // Initialize webhooks + if enableWebhooks && len(g.Webhooks) > 0 { + wm := GetWebhookManager(g.logger) + for _, config := range g.Webhooks { + wm.AddWebhook(config) + } + } + + // Initialize persistent storage + if enableStorage && g.StoragePath != "" { + storage, err := InitStorage(g.logger, StorageConfig{ + Path: g.StoragePath, + }) + if err != nil { + g.logger.Warn("Failed to initialize storage, continuing without persistence", + zap.Error(err), + ) + } else { + g.storage = storage + // Load existing bans from storage + if err := g.loadBansFromStorage(); err != nil { + g.logger.Warn("Failed to load bans from storage", zap.Error(err)) + } + } + } + + // Initialize GeoIP if configured + if g.GeoIPPath != "" { + geoIP, err := NewGeoIPLookup(g.GeoIPPath) + if err != nil { + g.logger.Warn("Failed to initialize GeoIP, country blocking disabled", + zap.Error(err), + ) + } else { + g.geoIP = geoIP + g.logger.Info("GeoIP initialized", + zap.Int("blocked_countries", len(g.BlockedCountries)), + zap.Int("allowed_countries", len(g.AllowedCountries)), + ) + } + } + + // Initialize enumeration detection with config if specified + if g.Enumeration != nil { + SetEnumerationConfig(*g.Enumeration) + g.logger.Info("Enumeration detection configured", + zap.Int("max_extensions", g.Enumeration.MaxExtensions), + zap.Int("sequential_threshold", g.Enumeration.SequentialThreshold), + zap.Duration("extension_window", g.Enumeration.ExtensionWindow), + ) + } + // Start cleanup goroutine go g.cleanupLoop(ctx) @@ -90,11 +170,38 @@ func (g *SIPGuardian) Provision(ctx caddy.Context) error { zap.Duration("find_time", time.Duration(g.FindTime)), zap.Duration("ban_time", time.Duration(g.BanTime)), zap.Int("whitelist_count", len(g.whitelistNets)), + zap.Bool("storage_enabled", g.storage != nil), + zap.Bool("geoip_enabled", g.geoIP != nil), + zap.Int("webhook_count", len(g.Webhooks)), + zap.Bool("enumeration_enabled", g.Enumeration != nil), ) return nil } +// loadBansFromStorage loads active bans from persistent storage +func (g *SIPGuardian) loadBansFromStorage() error { + if g.storage == nil { + return nil + } + + bans, err := g.storage.LoadActiveBans() + if err != nil { + return err + } + + g.mu.Lock() + defer g.mu.Unlock() + + for _, ban := range bans { + entry := ban // Create a copy + g.bannedIPs[entry.IP] = &entry + } + + g.logger.Info("Loaded bans from storage", zap.Int("count", len(bans))) + return nil +} + // IsWhitelisted checks if an IP is in the whitelist func (g *SIPGuardian) IsWhitelisted(ip string) bool { parsedIP := net.ParseIP(ip) @@ -103,12 +210,47 @@ func (g *SIPGuardian) IsWhitelisted(ip string) bool { } for _, network := range g.whitelistNets { if network.Contains(parsedIP) { + if enableMetrics { + RecordWhitelistedConnection() + } return true } } return false } +// IsCountryBlocked checks if an IP's country is blocked (or not in allowed list) +func (g *SIPGuardian) IsCountryBlocked(ip string) (bool, string) { + if g.geoIP == nil { + return false, "" + } + + country, err := g.geoIP.LookupCountry(ip) + if err != nil { + g.logger.Debug("GeoIP lookup failed", zap.String("ip", ip), zap.Error(err)) + return false, "" + } + + // If allowed countries are specified, only those are allowed + if len(g.AllowedCountries) > 0 { + for _, allowed := range g.AllowedCountries { + if country == allowed { + return false, country + } + } + return true, country // Not in allowed list + } + + // Check blocked countries + for _, blocked := range g.BlockedCountries { + if country == blocked { + return true, country + } + } + + return false, country +} + // IsBanned checks if an IP is currently banned func (g *SIPGuardian) IsBanned(ip string) bool { g.mu.RLock() @@ -154,6 +296,24 @@ func (g *SIPGuardian) RecordFailure(ip, reason string) bool { zap.Int("count", tracker.count), ) + // Record metrics + if enableMetrics { + RecordFailure(reason) + UpdateTrackedIPs(len(g.failureCounts)) + } + + // Record in storage (async) + if g.storage != nil { + go func() { + g.storage.RecordFailure(ip, reason, nil) + }() + } + + // Emit failure event via webhook + if enableWebhooks { + EmitFailureEvent(g.logger, ip, reason, tracker.count) + } + // Check if we should ban if tracker.count >= g.MaxFailures { g.banIP(ip, reason) @@ -168,13 +328,19 @@ func (g *SIPGuardian) banIP(ip, reason string) { now := time.Now() banDuration := time.Duration(g.BanTime) - g.bannedIPs[ip] = &BanEntry{ + hitCount := 0 + if tracker := g.failureCounts[ip]; tracker != nil { + hitCount = tracker.count + } + + entry := &BanEntry{ IP: ip, Reason: reason, BannedAt: now, ExpiresAt: now.Add(banDuration), - HitCount: g.failureCounts[ip].count, + HitCount: hitCount, } + g.bannedIPs[ip] = entry // Clear failure counter delete(g.failureCounts, ip) @@ -184,6 +350,25 @@ func (g *SIPGuardian) banIP(ip, reason string) { zap.String("reason", reason), zap.Duration("duration", banDuration), ) + + // Record metrics + if enableMetrics { + RecordBan() + } + + // Save to persistent storage + if g.storage != nil { + go func() { + if err := g.storage.SaveBan(entry); err != nil { + g.logger.Error("Failed to save ban to storage", zap.Error(err)) + } + }() + } + + // Emit webhook event + if enableWebhooks { + EmitBanEvent(g.logger, entry) + } } // UnbanIP manually removes an IP from the ban list @@ -191,9 +376,31 @@ func (g *SIPGuardian) UnbanIP(ip string) bool { g.mu.Lock() defer g.mu.Unlock() - if _, exists := g.bannedIPs[ip]; exists { + if entry, exists := g.bannedIPs[ip]; exists { + // Record ban duration for metrics + if enableMetrics { + duration := time.Since(entry.BannedAt).Seconds() + RecordBanDuration(duration) + RecordUnban() + } + delete(g.bannedIPs, ip) g.logger.Info("IP unbanned", zap.String("ip", ip)) + + // Update storage + if g.storage != nil { + go func() { + if err := g.storage.RemoveBan(ip, "manual_unban"); err != nil { + g.logger.Error("Failed to update storage on unban", zap.Error(err)) + } + }() + } + + // Emit webhook event + if enableWebhooks { + EmitUnbanEvent(g.logger, ip, "manual_unban") + } + return true } return false @@ -273,6 +480,29 @@ func (g *SIPGuardian) cleanup() { } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. +// Extended configuration options: +// +// sip_guardian { +// max_failures 5 +// find_time 10m +// ban_time 1h +// whitelist 10.0.0.0/8 192.168.0.0/16 +// +// # Persistent storage +// storage /var/lib/sip-guardian/guardian.db +// +// # GeoIP blocking (requires MaxMind database) +// geoip_db /path/to/GeoLite2-Country.mmdb +// block_countries CN RU KP +// allow_countries US CA GB # Alternative: only allow these +// +// # Webhook notifications +// webhook https://example.com/hook { +// events ban unban suspicious +// secret my-webhook-secret +// timeout 10s +// } +// } func (g *SIPGuardian) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { for d.NextBlock(0) { @@ -312,6 +542,80 @@ func (g *SIPGuardian) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { g.WhitelistCIDR = append(g.WhitelistCIDR, d.Val()) } + case "storage": + if !d.NextArg() { + return d.ArgErr() + } + g.StoragePath = d.Val() + + case "geoip_db": + if !d.NextArg() { + return d.ArgErr() + } + g.GeoIPPath = d.Val() + + case "block_countries": + for d.NextArg() { + country := d.Val() + // Support continent expansion (e.g., "AS" for all of Asia) + if expanded := ExpandContinentCode(country); expanded != nil { + g.BlockedCountries = append(g.BlockedCountries, expanded...) + } else { + g.BlockedCountries = append(g.BlockedCountries, country) + } + } + + case "allow_countries": + for d.NextArg() { + country := d.Val() + if expanded := ExpandContinentCode(country); expanded != nil { + g.AllowedCountries = append(g.AllowedCountries, expanded...) + } else { + g.AllowedCountries = append(g.AllowedCountries, country) + } + } + + case "webhook": + if !d.NextArg() { + return d.ArgErr() + } + webhook := WebhookConfig{ + URL: d.Val(), + } + // Parse webhook block if present + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "events": + webhook.Events = d.RemainingArgs() + case "secret": + if !d.NextArg() { + return d.ArgErr() + } + webhook.Secret = d.Val() + case "timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("invalid webhook timeout: %v", err) + } + webhook.Timeout = dur + case "header": + args := d.RemainingArgs() + if len(args) != 2 { + return d.Errf("header requires name and value") + } + if webhook.Headers == nil { + webhook.Headers = make(map[string]string) + } + webhook.Headers[args[0]] = args[1] + default: + return d.Errf("unknown webhook directive: %s", d.Val()) + } + } + g.Webhooks = append(g.Webhooks, webhook) + default: return d.Errf("unknown directive: %s", d.Val()) } diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..6b1c659 --- /dev/null +++ b/storage.go @@ -0,0 +1,608 @@ +package sipguardian + +import ( + "database/sql" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "go.uber.org/zap" + _ "modernc.org/sqlite" // Pure Go SQLite driver +) + +// StorageConfig holds persistent storage configuration +type StorageConfig struct { + // Path to the SQLite database file + Path string `json:"path,omitempty"` + + // SyncInterval for periodic state sync + SyncInterval time.Duration `json:"sync_interval,omitempty"` + + // RetainExpired keeps expired bans for analysis (default: 7 days) + RetainExpired time.Duration `json:"retain_expired,omitempty"` +} + +// Storage provides persistent storage for SIP Guardian state +type Storage struct { + db *sql.DB + logger *zap.Logger + config StorageConfig + mu sync.Mutex + done chan struct{} +} + +// Global storage instance +var ( + globalStorage *Storage + storageMu sync.Mutex +) + +// GetStorage returns the global storage instance +func GetStorage() *Storage { + storageMu.Lock() + defer storageMu.Unlock() + return globalStorage +} + +// InitStorage initializes the persistent storage +func InitStorage(logger *zap.Logger, config StorageConfig) (*Storage, error) { + storageMu.Lock() + defer storageMu.Unlock() + + if globalStorage != nil { + return globalStorage, nil + } + + // Set defaults + if config.Path == "" { + // Default to data directory + dataDir := os.Getenv("XDG_DATA_HOME") + if dataDir == "" { + homeDir, _ := os.UserHomeDir() + dataDir = filepath.Join(homeDir, ".local", "share") + } + config.Path = filepath.Join(dataDir, "sip-guardian", "guardian.db") + } + if config.SyncInterval == 0 { + config.SyncInterval = 30 * time.Second + } + if config.RetainExpired == 0 { + config.RetainExpired = 7 * 24 * time.Hour // 7 days + } + + // Ensure directory exists + dir := filepath.Dir(config.Path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("failed to create storage directory: %w", err) + } + + // Open database + db, err := sql.Open("sqlite", config.Path) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // Enable WAL mode for better concurrent access + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + db.Close() + return nil, fmt.Errorf("failed to set WAL mode: %w", err) + } + + storage := &Storage{ + db: db, + logger: logger, + config: config, + done: make(chan struct{}), + } + + // Initialize schema + if err := storage.initSchema(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to initialize schema: %w", err) + } + + globalStorage = storage + + logger.Info("Storage initialized", + zap.String("path", config.Path), + zap.Duration("sync_interval", config.SyncInterval), + ) + + return storage, nil +} + +// initSchema creates the database tables +func (s *Storage) initSchema() error { + schema := ` + CREATE TABLE IF NOT EXISTS bans ( + ip TEXT PRIMARY KEY, + reason TEXT NOT NULL, + banned_at DATETIME NOT NULL, + expires_at DATETIME NOT NULL, + hit_count INTEGER DEFAULT 0, + unbanned_at DATETIME, + unban_reason TEXT, + metadata TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_bans_expires ON bans(expires_at); + CREATE INDEX IF NOT EXISTS idx_bans_banned_at ON bans(banned_at); + + CREATE TABLE IF NOT EXISTS failures ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOT NULL, + reason TEXT NOT NULL, + recorded_at DATETIME NOT NULL, + metadata TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_failures_ip ON failures(ip); + CREATE INDEX IF NOT EXISTS idx_failures_recorded_at ON failures(recorded_at); + + CREATE TABLE IF NOT EXISTS stats ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL + ); + + CREATE TABLE IF NOT EXISTS suspicious_patterns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOT NULL, + pattern TEXT NOT NULL, + sample TEXT, + detected_at DATETIME NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_suspicious_ip ON suspicious_patterns(ip); + CREATE INDEX IF NOT EXISTS idx_suspicious_pattern ON suspicious_patterns(pattern); + + CREATE TABLE IF NOT EXISTS enumeration_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOT NULL, + reason TEXT NOT NULL, + unique_count INTEGER NOT NULL, + extensions TEXT, + detected_at DATETIME NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_enum_ip ON enumeration_attempts(ip); + CREATE INDEX IF NOT EXISTS idx_enum_detected ON enumeration_attempts(detected_at); + ` + + _, err := s.db.Exec(schema) + return err +} + +// SaveBan persists a ban entry +func (s *Storage) SaveBan(entry *BanEntry) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec(` + INSERT OR REPLACE INTO bans (ip, reason, banned_at, expires_at, hit_count) + VALUES (?, ?, ?, ?, ?) + `, entry.IP, entry.Reason, entry.BannedAt, entry.ExpiresAt, entry.HitCount) + + if err != nil { + s.logger.Error("Failed to save ban", zap.Error(err), zap.String("ip", entry.IP)) + return err + } + + s.logger.Debug("Ban saved to storage", zap.String("ip", entry.IP)) + return nil +} + +// RemoveBan marks a ban as unbanned (keeps for history) +func (s *Storage) RemoveBan(ip, reason string) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec(` + UPDATE bans SET unbanned_at = ?, unban_reason = ? WHERE ip = ? AND unbanned_at IS NULL + `, time.Now().UTC(), reason, ip) + + if err != nil { + s.logger.Error("Failed to remove ban", zap.Error(err), zap.String("ip", ip)) + return err + } + + s.logger.Debug("Ban removed from storage", zap.String("ip", ip)) + return nil +} + +// LoadActiveBans returns all currently active bans +func (s *Storage) LoadActiveBans() ([]BanEntry, error) { + s.mu.Lock() + defer s.mu.Unlock() + + rows, err := s.db.Query(` + SELECT ip, reason, banned_at, expires_at, hit_count + FROM bans + WHERE expires_at > ? AND unbanned_at IS NULL + `, time.Now().UTC()) + if err != nil { + return nil, fmt.Errorf("failed to query active bans: %w", err) + } + defer rows.Close() + + var bans []BanEntry + for rows.Next() { + var entry BanEntry + if err := rows.Scan(&entry.IP, &entry.Reason, &entry.BannedAt, &entry.ExpiresAt, &entry.HitCount); err != nil { + s.logger.Error("Failed to scan ban row", zap.Error(err)) + continue + } + bans = append(bans, entry) + } + + return bans, rows.Err() +} + +// RecordFailure records a failure event for historical analysis +func (s *Storage) RecordFailure(ip, reason string, metadata map[string]interface{}) error { + s.mu.Lock() + defer s.mu.Unlock() + + var metadataJSON []byte + if metadata != nil { + var err error + metadataJSON, err = json.Marshal(metadata) + if err != nil { + return fmt.Errorf("failed to marshal metadata: %w", err) + } + } + + _, err := s.db.Exec(` + INSERT INTO failures (ip, reason, recorded_at, metadata) + VALUES (?, ?, ?, ?) + `, ip, reason, time.Now().UTC(), metadataJSON) + + return err +} + +// RecordSuspiciousPattern records a suspicious pattern detection +func (s *Storage) RecordSuspiciousPattern(ip, pattern, sample string) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec(` + INSERT INTO suspicious_patterns (ip, pattern, sample, detected_at) + VALUES (?, ?, ?, ?) + `, ip, pattern, sample, time.Now().UTC()) + + return err +} + +// RecordEnumerationAttempt records an enumeration attack detection +func (s *Storage) RecordEnumerationAttempt(ip, reason string, uniqueCount int, extensions []string) error { + s.mu.Lock() + defer s.mu.Unlock() + + var extensionsJSON []byte + if extensions != nil { + var err error + extensionsJSON, err = json.Marshal(extensions) + if err != nil { + return fmt.Errorf("failed to marshal extensions: %w", err) + } + } + + _, err := s.db.Exec(` + INSERT INTO enumeration_attempts (ip, reason, unique_count, extensions, detected_at) + VALUES (?, ?, ?, ?, ?) + `, ip, reason, uniqueCount, extensionsJSON, time.Now().UTC()) + + if err != nil { + s.logger.Error("Failed to record enumeration attempt", + zap.Error(err), + zap.String("ip", ip), + zap.String("reason", reason), + ) + return err + } + + s.logger.Debug("Enumeration attempt recorded", + zap.String("ip", ip), + zap.String("reason", reason), + zap.Int("unique_extensions", uniqueCount), + ) + return nil +} + +// GetEnumerationStats returns statistics on enumeration attempts +func (s *Storage) GetEnumerationStats(since time.Duration) ([]map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + rows, err := s.db.Query(` + SELECT reason, COUNT(*) as count, COUNT(DISTINCT ip) as unique_ips, AVG(unique_count) as avg_extensions + FROM enumeration_attempts + WHERE detected_at > ? + GROUP BY reason + ORDER BY count DESC + `, time.Now().Add(-since).UTC()) + if err != nil { + return nil, fmt.Errorf("failed to query enumeration stats: %w", err) + } + defer rows.Close() + + var stats []map[string]interface{} + for rows.Next() { + var reason string + var count, uniqueIPs int + var avgExtensions float64 + + if err := rows.Scan(&reason, &count, &uniqueIPs, &avgExtensions); err != nil { + continue + } + + stats = append(stats, map[string]interface{}{ + "reason": reason, + "count": count, + "unique_ips": uniqueIPs, + "avg_extensions": avgExtensions, + }) + } + + return stats, rows.Err() +} + +// GetRecentEnumerationAttempts returns recent enumeration attempts +func (s *Storage) GetRecentEnumerationAttempts(since time.Duration, limit int) ([]map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if limit == 0 { + limit = 100 + } + + rows, err := s.db.Query(` + SELECT ip, reason, unique_count, extensions, detected_at + FROM enumeration_attempts + WHERE detected_at > ? + ORDER BY detected_at DESC + LIMIT ? + `, time.Now().Add(-since).UTC(), limit) + if err != nil { + return nil, fmt.Errorf("failed to query recent enumeration attempts: %w", err) + } + defer rows.Close() + + var attempts []map[string]interface{} + for rows.Next() { + var ip, reason string + var uniqueCount int + var extensionsJSON sql.NullString + var detectedAt time.Time + + if err := rows.Scan(&ip, &reason, &uniqueCount, &extensionsJSON, &detectedAt); err != nil { + continue + } + + entry := map[string]interface{}{ + "ip": ip, + "reason": reason, + "unique_count": uniqueCount, + "detected_at": detectedAt, + } + + if extensionsJSON.Valid { + var extensions []string + if err := json.Unmarshal([]byte(extensionsJSON.String), &extensions); err == nil { + entry["extensions"] = extensions + } + } + + attempts = append(attempts, entry) + } + + return attempts, rows.Err() +} + +// GetBanHistory returns ban history for an IP +func (s *Storage) GetBanHistory(ip string) ([]BanEntry, error) { + s.mu.Lock() + defer s.mu.Unlock() + + rows, err := s.db.Query(` + SELECT ip, reason, banned_at, expires_at, hit_count + FROM bans + WHERE ip = ? + ORDER BY banned_at DESC + `, ip) + if err != nil { + return nil, fmt.Errorf("failed to query ban history: %w", err) + } + defer rows.Close() + + var bans []BanEntry + for rows.Next() { + var entry BanEntry + if err := rows.Scan(&entry.IP, &entry.Reason, &entry.BannedAt, &entry.ExpiresAt, &entry.HitCount); err != nil { + continue + } + bans = append(bans, entry) + } + + return bans, rows.Err() +} + +// GetRecentFailures returns recent failures (for analysis) +func (s *Storage) GetRecentFailures(since time.Duration, limit int) ([]map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if limit == 0 { + limit = 100 + } + + rows, err := s.db.Query(` + SELECT ip, reason, recorded_at, metadata + FROM failures + WHERE recorded_at > ? + ORDER BY recorded_at DESC + LIMIT ? + `, time.Now().Add(-since).UTC(), limit) + if err != nil { + return nil, fmt.Errorf("failed to query recent failures: %w", err) + } + defer rows.Close() + + var failures []map[string]interface{} + for rows.Next() { + var ip, reason string + var recordedAt time.Time + var metadataJSON sql.NullString + + if err := rows.Scan(&ip, &reason, &recordedAt, &metadataJSON); err != nil { + continue + } + + entry := map[string]interface{}{ + "ip": ip, + "reason": reason, + "recorded_at": recordedAt, + } + + if metadataJSON.Valid { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(metadataJSON.String), &metadata); err == nil { + entry["metadata"] = metadata + } + } + + failures = append(failures, entry) + } + + return failures, rows.Err() +} + +// GetTopOffenders returns IPs with most failures/bans +func (s *Storage) GetTopOffenders(since time.Duration, limit int) ([]map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if limit == 0 { + limit = 10 + } + + rows, err := s.db.Query(` + SELECT ip, COUNT(*) as count, MAX(recorded_at) as last_seen + FROM failures + WHERE recorded_at > ? + GROUP BY ip + ORDER BY count DESC + LIMIT ? + `, time.Now().Add(-since).UTC(), limit) + if err != nil { + return nil, fmt.Errorf("failed to query top offenders: %w", err) + } + defer rows.Close() + + var offenders []map[string]interface{} + for rows.Next() { + var ip string + var count int + var lastSeen time.Time + + if err := rows.Scan(&ip, &count, &lastSeen); err != nil { + continue + } + + offenders = append(offenders, map[string]interface{}{ + "ip": ip, + "count": count, + "last_seen": lastSeen, + }) + } + + return offenders, rows.Err() +} + +// GetPatternStats returns statistics on detected patterns +func (s *Storage) GetPatternStats(since time.Duration) ([]map[string]interface{}, error) { + s.mu.Lock() + defer s.mu.Unlock() + + rows, err := s.db.Query(` + SELECT pattern, COUNT(*) as count, COUNT(DISTINCT ip) as unique_ips + FROM suspicious_patterns + WHERE detected_at > ? + GROUP BY pattern + ORDER BY count DESC + `, time.Now().Add(-since).UTC()) + if err != nil { + return nil, fmt.Errorf("failed to query pattern stats: %w", err) + } + defer rows.Close() + + var stats []map[string]interface{} + for rows.Next() { + var pattern string + var count, uniqueIPs int + + if err := rows.Scan(&pattern, &count, &uniqueIPs); err != nil { + continue + } + + stats = append(stats, map[string]interface{}{ + "pattern": pattern, + "count": count, + "unique_ips": uniqueIPs, + }) + } + + return stats, rows.Err() +} + +// Cleanup removes old data +func (s *Storage) Cleanup() error { + s.mu.Lock() + defer s.mu.Unlock() + + cutoff := time.Now().Add(-s.config.RetainExpired).UTC() + + // Remove old unbanned entries + _, err := s.db.Exec(` + DELETE FROM bans WHERE unbanned_at IS NOT NULL AND unbanned_at < ? + `, cutoff) + if err != nil { + return fmt.Errorf("failed to cleanup old bans: %w", err) + } + + // Remove old failures + _, err = s.db.Exec(` + DELETE FROM failures WHERE recorded_at < ? + `, cutoff) + if err != nil { + return fmt.Errorf("failed to cleanup old failures: %w", err) + } + + // Remove old suspicious patterns + _, err = s.db.Exec(` + DELETE FROM suspicious_patterns WHERE detected_at < ? + `, cutoff) + if err != nil { + return fmt.Errorf("failed to cleanup old patterns: %w", err) + } + + // Remove old enumeration attempts + _, err = s.db.Exec(` + DELETE FROM enumeration_attempts WHERE detected_at < ? + `, cutoff) + if err != nil { + return fmt.Errorf("failed to cleanup old enumeration attempts: %w", err) + } + + s.logger.Debug("Storage cleanup completed", zap.Time("cutoff", cutoff)) + return nil +} + +// Close closes the database connection +func (s *Storage) Close() error { + close(s.done) + return s.db.Close() +} diff --git a/webhooks.go b/webhooks.go new file mode 100644 index 0000000..4f96055 --- /dev/null +++ b/webhooks.go @@ -0,0 +1,344 @@ +package sipguardian + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "go.uber.org/zap" +) + +// WebhookEvent represents an event to be sent via webhook +type WebhookEvent struct { + Type string `json:"type"` + Timestamp time.Time `json:"timestamp"` + Data interface{} `json:"data"` +} + +// BanEventData contains data for ban/unban events +type BanEventData struct { + IP string `json:"ip"` + Reason string `json:"reason,omitempty"` + BannedAt time.Time `json:"banned_at,omitempty"` + ExpiresAt time.Time `json:"expires_at,omitempty"` + HitCount int `json:"hit_count,omitempty"` + Duration string `json:"duration,omitempty"` +} + +// SuspiciousEventData contains data for suspicious activity events +type SuspiciousEventData struct { + IP string `json:"ip"` + Pattern string `json:"pattern"` + Sample string `json:"sample,omitempty"` + FailureCount int `json:"failure_count"` +} + +// EnumerationEventData contains data for enumeration detection events +type EnumerationEventData struct { + IP string `json:"ip"` + Reason string `json:"reason"` + UniqueCount int `json:"unique_count"` + Extensions []string `json:"extensions,omitempty"` + SeqStart int `json:"seq_start,omitempty"` + SeqEnd int `json:"seq_end,omitempty"` +} + +// WebhookConfig holds webhook configuration +type WebhookConfig struct { + // URL to send webhook events to + URL string `json:"url"` + + // Secret for HMAC signature (optional) + Secret string `json:"secret,omitempty"` + + // Events to subscribe to (default: all) + // Options: "ban", "unban", "suspicious", "failure" + Events []string `json:"events,omitempty"` + + // Timeout for webhook requests + Timeout time.Duration `json:"timeout,omitempty"` + + // RetryCount for failed webhook deliveries + RetryCount int `json:"retry_count,omitempty"` + + // Headers to include in webhook requests + Headers map[string]string `json:"headers,omitempty"` +} + +// WebhookManager handles webhook dispatching +type WebhookManager struct { + configs []WebhookConfig + client *http.Client + logger *zap.Logger + mu sync.RWMutex + + // Channel for async event dispatching + eventChan chan WebhookEvent + done chan struct{} +} + +// Global webhook manager instance +var ( + webhookManager *WebhookManager + webhookMu sync.Mutex +) + +// GetWebhookManager returns the global webhook manager, creating it if necessary +func GetWebhookManager(logger *zap.Logger) *WebhookManager { + webhookMu.Lock() + defer webhookMu.Unlock() + + if webhookManager == nil { + webhookManager = &WebhookManager{ + configs: []WebhookConfig{}, + client: &http.Client{Timeout: 10 * time.Second}, + logger: logger, + eventChan: make(chan WebhookEvent, 100), + done: make(chan struct{}), + } + go webhookManager.dispatcher() + } + + return webhookManager +} + +// AddWebhook registers a new webhook endpoint +func (wm *WebhookManager) AddWebhook(config WebhookConfig) { + wm.mu.Lock() + defer wm.mu.Unlock() + + // Set defaults + if config.Timeout == 0 { + config.Timeout = 10 * time.Second + } + if config.RetryCount == 0 { + config.RetryCount = 3 + } + if len(config.Events) == 0 { + config.Events = []string{"ban", "unban", "suspicious", "failure", "enumeration"} + } + + wm.configs = append(wm.configs, config) + wm.logger.Info("Webhook registered", + zap.String("url", config.URL), + zap.Strings("events", config.Events), + ) +} + +// ClearWebhooks removes all registered webhooks +func (wm *WebhookManager) ClearWebhooks() { + wm.mu.Lock() + defer wm.mu.Unlock() + wm.configs = []WebhookConfig{} +} + +// Emit sends an event to all subscribed webhooks +func (wm *WebhookManager) Emit(eventType string, data interface{}) { + event := WebhookEvent{ + Type: eventType, + Timestamp: time.Now().UTC(), + Data: data, + } + + select { + case wm.eventChan <- event: + // Event queued + default: + wm.logger.Warn("Webhook event queue full, dropping event", + zap.String("type", eventType), + ) + } +} + +// dispatcher processes events from the channel +func (wm *WebhookManager) dispatcher() { + for { + select { + case <-wm.done: + return + case event := <-wm.eventChan: + wm.dispatch(event) + } + } +} + +// dispatch sends an event to all matching webhooks +func (wm *WebhookManager) dispatch(event WebhookEvent) { + wm.mu.RLock() + configs := make([]WebhookConfig, len(wm.configs)) + copy(configs, wm.configs) + wm.mu.RUnlock() + + for _, config := range configs { + if wm.shouldSend(config, event.Type) { + go wm.send(config, event) + } + } +} + +// shouldSend checks if an event type matches the webhook's subscriptions +func (wm *WebhookManager) shouldSend(config WebhookConfig, eventType string) bool { + for _, e := range config.Events { + if e == eventType || e == "all" { + return true + } + } + return false +} + +// send delivers a webhook event with retries +func (wm *WebhookManager) send(config WebhookConfig, event WebhookEvent) { + payload, err := json.Marshal(event) + if err != nil { + wm.logger.Error("Failed to marshal webhook event", + zap.Error(err), + zap.String("type", event.Type), + ) + return + } + + var lastErr error + for attempt := 0; attempt <= config.RetryCount; attempt++ { + if attempt > 0 { + // Exponential backoff + time.Sleep(time.Duration(attempt*attempt) * time.Second) + } + + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + req, err := http.NewRequestWithContext(ctx, "POST", config.URL, bytes.NewReader(payload)) + if err != nil { + cancel() + lastErr = err + continue + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "SIP-Guardian-Webhook/1.0") + req.Header.Set("X-SIP-Guardian-Event", event.Type) + + // Add custom headers + for k, v := range config.Headers { + req.Header.Set(k, v) + } + + // Add HMAC signature if secret is configured + if config.Secret != "" { + signature := computeHMAC(payload, config.Secret) + req.Header.Set("X-SIP-Guardian-Signature", signature) + } + + resp, err := wm.client.Do(req) + cancel() + + if err != nil { + lastErr = err + wm.logger.Debug("Webhook delivery failed, retrying", + zap.String("url", config.URL), + zap.Int("attempt", attempt+1), + zap.Error(err), + ) + continue + } + + resp.Body.Close() + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + wm.logger.Debug("Webhook delivered successfully", + zap.String("url", config.URL), + zap.String("type", event.Type), + zap.Int("status", resp.StatusCode), + ) + return + } + + lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode) + wm.logger.Debug("Webhook returned non-success status", + zap.String("url", config.URL), + zap.Int("status", resp.StatusCode), + zap.Int("attempt", attempt+1), + ) + } + + wm.logger.Error("Webhook delivery failed after retries", + zap.String("url", config.URL), + zap.String("type", event.Type), + zap.Error(lastErr), + ) +} + +// Stop gracefully shuts down the webhook manager +func (wm *WebhookManager) Stop() { + close(wm.done) +} + +// computeHMAC generates an HMAC-SHA256 signature for webhook verification +func computeHMAC(payload []byte, secret string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(payload) + return hex.EncodeToString(mac.Sum(nil)) +} + +// Helper functions for emitting specific events + +// EmitBanEvent sends a ban notification +func EmitBanEvent(logger *zap.Logger, entry *BanEntry) { + wm := GetWebhookManager(logger) + wm.Emit("ban", BanEventData{ + IP: entry.IP, + Reason: entry.Reason, + BannedAt: entry.BannedAt, + ExpiresAt: entry.ExpiresAt, + HitCount: entry.HitCount, + Duration: entry.ExpiresAt.Sub(entry.BannedAt).String(), + }) +} + +// EmitUnbanEvent sends an unban notification +func EmitUnbanEvent(logger *zap.Logger, ip string, reason string) { + wm := GetWebhookManager(logger) + wm.Emit("unban", BanEventData{ + IP: ip, + Reason: reason, + }) +} + +// EmitSuspiciousEvent sends a suspicious activity notification +func EmitSuspiciousEvent(logger *zap.Logger, ip, pattern, sample string, failureCount int) { + wm := GetWebhookManager(logger) + wm.Emit("suspicious", SuspiciousEventData{ + IP: ip, + Pattern: pattern, + Sample: sample, + FailureCount: failureCount, + }) +} + +// EmitFailureEvent sends a failure notification +func EmitFailureEvent(logger *zap.Logger, ip, reason string, count int) { + wm := GetWebhookManager(logger) + wm.Emit("failure", map[string]interface{}{ + "ip": ip, + "reason": reason, + "count": count, + }) +} + +// EmitEnumerationEvent sends an enumeration detection notification +func EmitEnumerationEvent(logger *zap.Logger, ip string, result EnumerationResult) { + wm := GetWebhookManager(logger) + wm.Emit("enumeration", EnumerationEventData{ + IP: ip, + Reason: result.Reason, + UniqueCount: result.UniqueCount, + Extensions: result.Extensions, + SeqStart: result.SeqStart, + SeqEnd: result.SeqEnd, + }) +}