Compare commits
2 Commits
b5fa007d6e
...
c73fa9d3d1
| Author | SHA1 | Date | |
|---|---|---|---|
| c73fa9d3d1 | |||
| 0b0fb53c9c |
24
Dockerfile
24
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"]
|
||||
|
||||
180
Makefile
180
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: <sip:test@localhost>;tag=123\r\nTo: <sip:test@localhost>\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"
|
||||
|
||||
45
admin.go
45
admin.go
@ -6,11 +6,23 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
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
|
||||
func parseSIPGuardianAdmin(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var handler AdminHandler
|
||||
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
||||
return &handler, err
|
||||
}
|
||||
|
||||
// AdminHandler provides HTTP endpoints to manage SIP Guardian
|
||||
@ -26,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
|
||||
@ -160,8 +175,30 @@ func (h *AdminHandler) handleBan(w http.ResponseWriter, r *http.Request, path st
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler for AdminHandler.
|
||||
// Usage in Caddyfile:
|
||||
//
|
||||
// handle /api/sip-guardian/* {
|
||||
// sip_guardian_admin
|
||||
// }
|
||||
//
|
||||
// Or simply: sip_guardian_admin
|
||||
func (h *AdminHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// Move past "sip_guardian_admin" token
|
||||
d.Next()
|
||||
|
||||
// This handler doesn't have any configuration options currently
|
||||
// but we need to consume any block if present
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
return d.Errf("unknown sip_guardian_admin directive: %s", d.Val())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddyhttp.MiddlewareHandler = (*AdminHandler)(nil)
|
||||
_ caddy.Provisioner = (*AdminHandler)(nil)
|
||||
_ caddyfile.Unmarshaler = (*AdminHandler)(nil)
|
||||
)
|
||||
|
||||
365
enumeration.go
Normal file
365
enumeration.go
Normal file
@ -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)
|
||||
}
|
||||
432
enumeration_test.go
Normal file
432
enumeration_test.go
Normal file
@ -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: <sip:caller@example.com>\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: <sip:500@example.com>\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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
196
geoip.go
Normal file
196
geoip.go
Normal file
@ -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
|
||||
}
|
||||
168
go.mod
168
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
|
||||
)
|
||||
|
||||
673
go.sum
673
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=
|
||||
|
||||
417
l4handler.go
417
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: <sip:100@", // Common test extension
|
||||
"From: <sip:1000@",
|
||||
"To: <sip:100@",
|
||||
}
|
||||
// detectSuspiciousPattern checks for common attack patterns and returns the pattern name
|
||||
func detectSuspiciousPattern(data []byte) string {
|
||||
lower := strings.ToLower(string(data))
|
||||
|
||||
lower := strings.ToLower(s)
|
||||
for _, pattern := range suspiciousPatterns {
|
||||
if strings.Contains(lower, strings.ToLower(pattern)) {
|
||||
return true
|
||||
for _, def := range suspiciousPatternDefs {
|
||||
if strings.Contains(lower, def.pattern) {
|
||||
return def.name
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
// isSuspiciousSIP checks for common attack patterns in SIP traffic (legacy wrapper)
|
||||
func isSuspiciousSIP(data []byte) bool {
|
||||
return detectSuspiciousPattern(data) != ""
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
@ -214,6 +349,20 @@ func (m *SIPMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// find_time 10m
|
||||
// ban_time 1h
|
||||
// whitelist 10.0.0.0/8 172.16.0.0/12
|
||||
// storage /data/sip-guardian.db
|
||||
// geoip_db /data/GeoLite2-Country.mmdb
|
||||
// block_countries CN RU
|
||||
// allow_countries US CA GB
|
||||
// enumeration {
|
||||
// max_extensions 20
|
||||
// extension_window 5m
|
||||
// sequential_threshold 5
|
||||
// rapid_fire_count 10
|
||||
// rapid_fire_window 30s
|
||||
// ban_time 2h
|
||||
// exempt_extensions 100 200 9999
|
||||
// }
|
||||
// webhook http://example.com/hook { ... }
|
||||
// }
|
||||
//
|
||||
// Or simply: sip_guardian (uses defaults)
|
||||
@ -221,18 +370,184 @@ func (h *SIPHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// Move past "sip_guardian" token
|
||||
d.Next()
|
||||
|
||||
// The handler doesn't have its own configuration - it uses the shared SIPGuardian
|
||||
// But we need to parse any configuration blocks that might be present
|
||||
// Parse configuration into the embedded SIPGuardian struct
|
||||
// This config will be applied to the shared guardian during Provision
|
||||
|
||||
// Check for inline args or block
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
// For now, SIPHandler delegates to SIPGuardian
|
||||
// In future, handler-specific config could go here
|
||||
switch d.Val() {
|
||||
case "max_failures", "find_time", "ban_time", "whitelist":
|
||||
// These are handled by the embedded SIPGuardian
|
||||
// Skip to allow flexibility in config placement
|
||||
d.RemainingArgs()
|
||||
case "max_failures":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(d.Val(), "%d", &val); err != nil {
|
||||
return d.Errf("invalid max_failures: %v", err)
|
||||
}
|
||||
h.MaxFailures = val
|
||||
|
||||
case "find_time":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid find_time: %v", err)
|
||||
}
|
||||
h.FindTime = caddy.Duration(dur)
|
||||
|
||||
case "ban_time":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid ban_time: %v", err)
|
||||
}
|
||||
h.BanTime = caddy.Duration(dur)
|
||||
|
||||
case "whitelist":
|
||||
for d.NextArg() {
|
||||
h.WhitelistCIDR = append(h.WhitelistCIDR, d.Val())
|
||||
}
|
||||
|
||||
case "storage":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.StoragePath = d.Val()
|
||||
|
||||
case "geoip_db":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.GeoIPPath = d.Val()
|
||||
|
||||
case "block_countries":
|
||||
for d.NextArg() {
|
||||
country := d.Val()
|
||||
if expanded := ExpandContinentCode(country); expanded != nil {
|
||||
h.BlockedCountries = append(h.BlockedCountries, expanded...)
|
||||
} else {
|
||||
h.BlockedCountries = append(h.BlockedCountries, country)
|
||||
}
|
||||
}
|
||||
|
||||
case "allow_countries":
|
||||
for d.NextArg() {
|
||||
country := d.Val()
|
||||
if expanded := ExpandContinentCode(country); expanded != nil {
|
||||
h.AllowedCountries = append(h.AllowedCountries, expanded...)
|
||||
} else {
|
||||
h.AllowedCountries = append(h.AllowedCountries, country)
|
||||
}
|
||||
}
|
||||
|
||||
case "enumeration":
|
||||
h.Enumeration = &EnumerationConfig{}
|
||||
for innerNesting := d.Nesting(); d.NextBlock(innerNesting); {
|
||||
switch d.Val() {
|
||||
case "max_extensions":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(d.Val(), "%d", &val); err != nil {
|
||||
return d.Errf("invalid max_extensions: %v", err)
|
||||
}
|
||||
h.Enumeration.MaxExtensions = val
|
||||
case "extension_window":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid extension_window: %v", err)
|
||||
}
|
||||
h.Enumeration.ExtensionWindow = dur
|
||||
case "sequential_threshold":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(d.Val(), "%d", &val); err != nil {
|
||||
return d.Errf("invalid sequential_threshold: %v", err)
|
||||
}
|
||||
h.Enumeration.SequentialThreshold = val
|
||||
case "rapid_fire_count":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(d.Val(), "%d", &val); err != nil {
|
||||
return d.Errf("invalid rapid_fire_count: %v", err)
|
||||
}
|
||||
h.Enumeration.RapidFireCount = val
|
||||
case "rapid_fire_window":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid rapid_fire_window: %v", err)
|
||||
}
|
||||
h.Enumeration.RapidFireWindow = dur
|
||||
case "ban_time":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid enumeration ban_time: %v", err)
|
||||
}
|
||||
h.Enumeration.EnumBanTime = dur
|
||||
case "exempt_extensions":
|
||||
h.Enumeration.ExemptExtensions = d.RemainingArgs()
|
||||
default:
|
||||
return d.Errf("unknown enumeration directive: %s", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "webhook":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
webhook := WebhookConfig{
|
||||
URL: d.Val(),
|
||||
}
|
||||
// Parse webhook block if present
|
||||
for innerNesting := d.Nesting(); d.NextBlock(innerNesting); {
|
||||
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())
|
||||
}
|
||||
}
|
||||
h.Webhooks = append(h.Webhooks, webhook)
|
||||
|
||||
default:
|
||||
return d.Errf("unknown sip_guardian directive: %s", d.Val())
|
||||
}
|
||||
|
||||
287
metrics.go
Normal file
287
metrics.go
Normal file
@ -0,0 +1,287 @@
|
||||
package sipguardian
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(MetricsHandler{})
|
||||
httpcaddyfile.RegisterHandlerDirective("sip_guardian_metrics", parseSIPGuardianMetrics)
|
||||
httpcaddyfile.RegisterDirectiveOrder("sip_guardian_metrics", httpcaddyfile.Before, "respond")
|
||||
}
|
||||
|
||||
// Prometheus metrics for SIP Guardian
|
||||
var (
|
||||
sipConnectionsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "connections_total",
|
||||
Help: "Total number of SIP connections processed",
|
||||
},
|
||||
[]string{"status"}, // "allowed", "blocked", "suspicious"
|
||||
)
|
||||
|
||||
sipBansTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "bans_total",
|
||||
Help: "Total number of IP bans issued",
|
||||
},
|
||||
)
|
||||
|
||||
sipUnbansTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "unbans_total",
|
||||
Help: "Total number of IP unbans (manual or expired)",
|
||||
},
|
||||
)
|
||||
|
||||
sipActiveBans = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "active_bans",
|
||||
Help: "Current number of active IP bans",
|
||||
},
|
||||
)
|
||||
|
||||
sipFailuresTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "failures_total",
|
||||
Help: "Total number of recorded failures by reason",
|
||||
},
|
||||
[]string{"reason"},
|
||||
)
|
||||
|
||||
sipTrackedIPs = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "tracked_ips",
|
||||
Help: "Current number of IPs being tracked for failures",
|
||||
},
|
||||
)
|
||||
|
||||
sipWhitelistedConnections = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "whitelisted_connections_total",
|
||||
Help: "Total connections from whitelisted IPs",
|
||||
},
|
||||
)
|
||||
|
||||
sipSuspiciousPatterns = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "suspicious_patterns_total",
|
||||
Help: "Total suspicious patterns detected by type",
|
||||
},
|
||||
[]string{"pattern"},
|
||||
)
|
||||
|
||||
sipBanDurationSeconds = prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "ban_duration_seconds",
|
||||
Help: "Distribution of ban durations in seconds",
|
||||
Buckets: []float64{60, 300, 600, 1800, 3600, 7200, 14400, 28800, 86400},
|
||||
},
|
||||
)
|
||||
|
||||
// Enumeration detection metrics
|
||||
sipEnumerationDetections = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "enumeration_detections_total",
|
||||
Help: "Total enumeration attacks detected by reason",
|
||||
},
|
||||
[]string{"reason"}, // "extension_count_exceeded", "sequential_enumeration", "rapid_fire_enumeration"
|
||||
)
|
||||
|
||||
sipEnumerationTrackedIPs = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "enumeration_tracked_ips",
|
||||
Help: "Current number of IPs being tracked for enumeration",
|
||||
},
|
||||
)
|
||||
|
||||
sipEnumerationUniqueExtensions = prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "sip_guardian",
|
||||
Name: "enumeration_unique_extensions",
|
||||
Help: "Distribution of unique extensions per IP at detection time",
|
||||
Buckets: []float64{5, 10, 15, 20, 30, 50, 100},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// metricsRegistered tracks if we've registered with Prometheus
|
||||
var metricsRegistered bool
|
||||
|
||||
// RegisterMetrics registers all SIP Guardian metrics with Prometheus
|
||||
func RegisterMetrics() {
|
||||
if metricsRegistered {
|
||||
return
|
||||
}
|
||||
metricsRegistered = true
|
||||
|
||||
prometheus.MustRegister(
|
||||
sipConnectionsTotal,
|
||||
sipBansTotal,
|
||||
sipUnbansTotal,
|
||||
sipActiveBans,
|
||||
sipFailuresTotal,
|
||||
sipTrackedIPs,
|
||||
sipWhitelistedConnections,
|
||||
sipSuspiciousPatterns,
|
||||
sipBanDurationSeconds,
|
||||
sipEnumerationDetections,
|
||||
sipEnumerationTrackedIPs,
|
||||
sipEnumerationUniqueExtensions,
|
||||
)
|
||||
}
|
||||
|
||||
// Metric recording functions - called from other modules
|
||||
|
||||
// RecordConnection records a connection event
|
||||
func RecordConnection(status string) {
|
||||
sipConnectionsTotal.WithLabelValues(status).Inc()
|
||||
}
|
||||
|
||||
// RecordBan records a ban event
|
||||
func RecordBan() {
|
||||
sipBansTotal.Inc()
|
||||
sipActiveBans.Inc()
|
||||
}
|
||||
|
||||
// RecordUnban records an unban event
|
||||
func RecordUnban() {
|
||||
sipUnbansTotal.Inc()
|
||||
sipActiveBans.Dec()
|
||||
}
|
||||
|
||||
// RecordFailure records a failure event
|
||||
func RecordFailure(reason string) {
|
||||
sipFailuresTotal.WithLabelValues(reason).Inc()
|
||||
}
|
||||
|
||||
// RecordWhitelistedConnection records a whitelisted connection
|
||||
func RecordWhitelistedConnection() {
|
||||
sipWhitelistedConnections.Inc()
|
||||
}
|
||||
|
||||
// RecordSuspiciousPattern records a suspicious pattern detection
|
||||
func RecordSuspiciousPattern(pattern string) {
|
||||
sipSuspiciousPatterns.WithLabelValues(pattern).Inc()
|
||||
}
|
||||
|
||||
// RecordBanDuration records the duration of a ban when it expires
|
||||
func RecordBanDuration(seconds float64) {
|
||||
sipBanDurationSeconds.Observe(seconds)
|
||||
}
|
||||
|
||||
// UpdateActiveBans updates the active bans gauge
|
||||
func UpdateActiveBans(count int) {
|
||||
sipActiveBans.Set(float64(count))
|
||||
}
|
||||
|
||||
// UpdateTrackedIPs updates the tracked IPs gauge
|
||||
func UpdateTrackedIPs(count int) {
|
||||
sipTrackedIPs.Set(float64(count))
|
||||
}
|
||||
|
||||
// RecordEnumerationDetection records an enumeration attack detection
|
||||
func RecordEnumerationDetection(reason string) {
|
||||
sipEnumerationDetections.WithLabelValues(reason).Inc()
|
||||
}
|
||||
|
||||
// UpdateEnumerationTrackedIPs updates the enumeration tracked IPs gauge
|
||||
func UpdateEnumerationTrackedIPs(count int) {
|
||||
sipEnumerationTrackedIPs.Set(float64(count))
|
||||
}
|
||||
|
||||
// RecordEnumerationExtensions records the number of unique extensions at detection
|
||||
func RecordEnumerationExtensions(count int) {
|
||||
sipEnumerationUniqueExtensions.Observe(float64(count))
|
||||
}
|
||||
|
||||
// MetricsHandler provides a Prometheus metrics endpoint for SIP Guardian
|
||||
type MetricsHandler struct {
|
||||
// Path prefix for metrics (default: /metrics)
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
func (MetricsHandler) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.sip_guardian_metrics",
|
||||
New: func() caddy.Module { return new(MetricsHandler) },
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) Provision(ctx caddy.Context) error {
|
||||
RegisterMetrics()
|
||||
|
||||
if h.Path == "" {
|
||||
h.Path = "/metrics"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP serves the Prometheus metrics
|
||||
func (h *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
// Update gauges from current state
|
||||
if guardian := GetGuardian("default"); guardian != nil {
|
||||
stats := guardian.GetStats()
|
||||
if activeBans, ok := stats["active_bans"].(int); ok {
|
||||
UpdateActiveBans(activeBans)
|
||||
}
|
||||
if trackedFailures, ok := stats["tracked_failures"].(int); ok {
|
||||
UpdateTrackedIPs(trackedFailures)
|
||||
}
|
||||
}
|
||||
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseSIPGuardianMetrics parses the sip_guardian_metrics directive
|
||||
func parseSIPGuardianMetrics(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var handler MetricsHandler
|
||||
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
||||
return &handler, err
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler for MetricsHandler.
|
||||
func (h *MetricsHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume directive name
|
||||
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "path":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.Path = d.Val()
|
||||
default:
|
||||
return d.Errf("unknown sip_guardian_metrics directive: %s", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddyhttp.MiddlewareHandler = (*MetricsHandler)(nil)
|
||||
_ caddy.Provisioner = (*MetricsHandler)(nil)
|
||||
_ caddyfile.Unmarshaler = (*MetricsHandler)(nil)
|
||||
)
|
||||
360
ratelimit.go
Normal file
360
ratelimit.go
Normal file
@ -0,0 +1,360 @@
|
||||
package sipguardian
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SIPMethod represents a SIP request method
|
||||
type SIPMethod string
|
||||
|
||||
const (
|
||||
MethodREGISTER SIPMethod = "REGISTER"
|
||||
MethodINVITE SIPMethod = "INVITE"
|
||||
MethodOPTIONS SIPMethod = "OPTIONS"
|
||||
MethodACK SIPMethod = "ACK"
|
||||
MethodBYE SIPMethod = "BYE"
|
||||
MethodCANCEL SIPMethod = "CANCEL"
|
||||
MethodINFO SIPMethod = "INFO"
|
||||
MethodNOTIFY SIPMethod = "NOTIFY"
|
||||
MethodSUBSCRIBE SIPMethod = "SUBSCRIBE"
|
||||
MethodMESSAGE SIPMethod = "MESSAGE"
|
||||
MethodUPDATE SIPMethod = "UPDATE"
|
||||
MethodPRACK SIPMethod = "PRACK"
|
||||
MethodREFER SIPMethod = "REFER"
|
||||
MethodPUBLISH SIPMethod = "PUBLISH"
|
||||
)
|
||||
|
||||
// MethodRateLimit defines rate limits per SIP method
|
||||
type MethodRateLimit struct {
|
||||
// Method to rate limit
|
||||
Method SIPMethod `json:"method"`
|
||||
|
||||
// MaxRequests per time window
|
||||
MaxRequests int `json:"max_requests"`
|
||||
|
||||
// Window is the time window for rate limiting
|
||||
Window time.Duration `json:"window"`
|
||||
|
||||
// BurstSize allows temporary bursts above the rate
|
||||
BurstSize int `json:"burst_size,omitempty"`
|
||||
}
|
||||
|
||||
// RateLimiter provides per-IP, per-method rate limiting
|
||||
type RateLimiter struct {
|
||||
limits map[SIPMethod]*MethodRateLimit
|
||||
buckets map[string]*methodBuckets
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
|
||||
// Default limits (used when no specific limit configured)
|
||||
defaultMaxRequests int
|
||||
defaultWindow time.Duration
|
||||
}
|
||||
|
||||
// methodBuckets tracks request counts per method for an IP
|
||||
type methodBuckets struct {
|
||||
methods map[SIPMethod]*tokenBucket
|
||||
lastReset time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// tokenBucket implements a simple token bucket algorithm
|
||||
type tokenBucket struct {
|
||||
tokens float64
|
||||
lastUpdate time.Time
|
||||
maxTokens float64
|
||||
refillRate float64 // tokens per second
|
||||
}
|
||||
|
||||
// Global rate limiter instance
|
||||
var (
|
||||
globalRateLimiter *RateLimiter
|
||||
rateLimiterMu sync.Mutex
|
||||
)
|
||||
|
||||
// DefaultMethodLimits provides reasonable default rate limits per method
|
||||
var DefaultMethodLimits = map[SIPMethod]*MethodRateLimit{
|
||||
MethodREGISTER: {
|
||||
Method: MethodREGISTER,
|
||||
MaxRequests: 10,
|
||||
Window: time.Minute,
|
||||
BurstSize: 3,
|
||||
},
|
||||
MethodINVITE: {
|
||||
Method: MethodINVITE,
|
||||
MaxRequests: 30,
|
||||
Window: time.Minute,
|
||||
BurstSize: 5,
|
||||
},
|
||||
MethodOPTIONS: {
|
||||
Method: MethodOPTIONS,
|
||||
MaxRequests: 60,
|
||||
Window: time.Minute,
|
||||
BurstSize: 10,
|
||||
},
|
||||
MethodSUBSCRIBE: {
|
||||
Method: MethodSUBSCRIBE,
|
||||
MaxRequests: 20,
|
||||
Window: time.Minute,
|
||||
BurstSize: 5,
|
||||
},
|
||||
MethodMESSAGE: {
|
||||
Method: MethodMESSAGE,
|
||||
MaxRequests: 100,
|
||||
Window: time.Minute,
|
||||
BurstSize: 20,
|
||||
},
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter
|
||||
func NewRateLimiter(logger *zap.Logger) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
limits: make(map[SIPMethod]*MethodRateLimit),
|
||||
buckets: make(map[string]*methodBuckets),
|
||||
logger: logger,
|
||||
defaultMaxRequests: 100,
|
||||
defaultWindow: time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRateLimiter returns the global rate limiter
|
||||
func GetRateLimiter(logger *zap.Logger) *RateLimiter {
|
||||
rateLimiterMu.Lock()
|
||||
defer rateLimiterMu.Unlock()
|
||||
|
||||
if globalRateLimiter == nil {
|
||||
globalRateLimiter = NewRateLimiter(logger)
|
||||
// Apply default limits
|
||||
for method, limit := range DefaultMethodLimits {
|
||||
globalRateLimiter.SetLimit(method, limit)
|
||||
}
|
||||
}
|
||||
|
||||
return globalRateLimiter
|
||||
}
|
||||
|
||||
// SetLimit configures a rate limit for a specific method
|
||||
func (rl *RateLimiter) SetLimit(method SIPMethod, limit *MethodRateLimit) {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
rl.limits[method] = limit
|
||||
}
|
||||
|
||||
// GetLimit returns the rate limit for a method
|
||||
func (rl *RateLimiter) GetLimit(method SIPMethod) *MethodRateLimit {
|
||||
rl.mu.RLock()
|
||||
defer rl.mu.RUnlock()
|
||||
|
||||
if limit, ok := rl.limits[method]; ok {
|
||||
return limit
|
||||
}
|
||||
|
||||
// Return default limit
|
||||
return &MethodRateLimit{
|
||||
Method: method,
|
||||
MaxRequests: rl.defaultMaxRequests,
|
||||
Window: rl.defaultWindow,
|
||||
}
|
||||
}
|
||||
|
||||
// Allow checks if a request should be allowed based on rate limits
|
||||
// Returns (allowed, reason) - if not allowed, reason explains why
|
||||
func (rl *RateLimiter) Allow(ip string, method SIPMethod) (bool, string) {
|
||||
rl.mu.Lock()
|
||||
bucket, exists := rl.buckets[ip]
|
||||
if !exists {
|
||||
bucket = &methodBuckets{
|
||||
methods: make(map[SIPMethod]*tokenBucket),
|
||||
lastReset: time.Now(),
|
||||
}
|
||||
rl.buckets[ip] = bucket
|
||||
}
|
||||
rl.mu.Unlock()
|
||||
|
||||
limit := rl.GetLimit(method)
|
||||
|
||||
bucket.mu.Lock()
|
||||
defer bucket.mu.Unlock()
|
||||
|
||||
tb, exists := bucket.methods[method]
|
||||
if !exists {
|
||||
// Create new token bucket for this method
|
||||
burstSize := limit.BurstSize
|
||||
if burstSize == 0 {
|
||||
burstSize = limit.MaxRequests / 5 // Default burst is 20% of max
|
||||
if burstSize < 1 {
|
||||
burstSize = 1
|
||||
}
|
||||
}
|
||||
|
||||
tb = &tokenBucket{
|
||||
tokens: float64(burstSize),
|
||||
lastUpdate: time.Now(),
|
||||
maxTokens: float64(burstSize),
|
||||
refillRate: float64(limit.MaxRequests) / limit.Window.Seconds(),
|
||||
}
|
||||
bucket.methods[method] = tb
|
||||
}
|
||||
|
||||
// Refill tokens based on elapsed time
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(tb.lastUpdate).Seconds()
|
||||
tb.tokens += elapsed * tb.refillRate
|
||||
if tb.tokens > 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 ""
|
||||
}
|
||||
172
registry.go
172
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 == "" {
|
||||
|
||||
107
sandbox/Caddyfile
Normal file
107
sandbox/Caddyfile
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
219
sandbox/docker-compose.yml
Normal file
219
sandbox/docker-compose.yml
Normal file
@ -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
|
||||
123
sandbox/scripts/bruteforce.py
Normal file
123
sandbox/scripts/bruteforce.py
Normal file
@ -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: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 1 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\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()
|
||||
168
sandbox/scripts/valid_register.py
Normal file
168
sandbox/scripts/valid_register.py
Normal file
@ -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: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 2 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\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: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 1 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\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()
|
||||
318
sipguardian.go
318
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())
|
||||
}
|
||||
|
||||
608
storage.go
Normal file
608
storage.go
Normal file
@ -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()
|
||||
}
|
||||
344
webhooks.go
Normal file
344
webhooks.go
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user