Files
traefikoidc/security_monitoring.go
T
2025-06-20 08:02:26 +01:00

572 lines
17 KiB
Go

package traefikoidc
import (
"fmt"
"maps"
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
// SecurityEvent represents a security-related event that should be logged and monitored
type SecurityEvent struct {
Type string `json:"type"`
Severity string `json:"severity"`
Timestamp time.Time `json:"timestamp"`
ClientIP string `json:"client_ip"`
UserAgent string `json:"user_agent"`
RequestPath string `json:"request_path"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
// SecurityMonitor tracks security events and suspicious activity patterns
type SecurityMonitor struct {
// Event counters
authFailures int64
tokenValidationFails int64
rateLimitHits int64
suspiciousRequests int64
// IP-based tracking
ipFailures map[string]*IPFailureTracker
ipMutex sync.RWMutex
// Pattern detection
patternDetector *SuspiciousPatternDetector
// Event handlers
eventHandlers []SecurityEventHandler
// Configuration
config SecurityMonitorConfig
// Logger
logger *Logger
}
// IPFailureTracker tracks failures for a specific IP address
type IPFailureTracker struct {
FailureCount int64
LastFailure time.Time
FirstFailure time.Time
FailureTypes map[string]int64
IsBlocked bool
BlockedUntil time.Time
mutex sync.RWMutex
}
// SuspiciousPatternDetector identifies patterns that may indicate attacks
type SuspiciousPatternDetector struct {
// Time-based windows for pattern detection
shortWindow time.Duration // 1 minute
mediumWindow time.Duration // 5 minutes
longWindow time.Duration // 15 minutes
// Pattern thresholds
rapidFailureThreshold int // failures in short window
distributedAttackThreshold int // failures across IPs in medium window
persistentAttackThreshold int // failures in long window
// Pattern tracking
recentEvents []SecurityEvent
eventsMutex sync.RWMutex
}
// SecurityEventHandler defines the interface for handling security events
type SecurityEventHandler interface {
HandleSecurityEvent(event SecurityEvent)
}
// SecurityMonitorConfig contains configuration for the security monitor
type SecurityMonitorConfig struct {
// Failure thresholds
MaxFailuresPerIP int `json:"max_failures_per_ip"`
FailureWindowMinutes int `json:"failure_window_minutes"`
BlockDurationMinutes int `json:"block_duration_minutes"`
// Pattern detection settings
EnablePatternDetection bool `json:"enable_pattern_detection"`
RapidFailureThreshold int `json:"rapid_failure_threshold"`
// Monitoring settings
EnableDetailedLogging bool `json:"enable_detailed_logging"`
LogSuspiciousOnly bool `json:"log_suspicious_only"`
// Cleanup settings
CleanupIntervalMinutes int `json:"cleanup_interval_minutes"`
RetentionHours int `json:"retention_hours"`
}
// DefaultSecurityMonitorConfig returns a default configuration
func DefaultSecurityMonitorConfig() SecurityMonitorConfig {
return SecurityMonitorConfig{
MaxFailuresPerIP: 10,
FailureWindowMinutes: 15,
BlockDurationMinutes: 60,
EnablePatternDetection: true,
RapidFailureThreshold: 5,
EnableDetailedLogging: true,
LogSuspiciousOnly: false,
CleanupIntervalMinutes: 30,
RetentionHours: 24,
}
}
// NewSecurityMonitor creates a new security monitor instance
func NewSecurityMonitor(config SecurityMonitorConfig, logger *Logger) *SecurityMonitor {
sm := &SecurityMonitor{
ipFailures: make(map[string]*IPFailureTracker),
eventHandlers: make([]SecurityEventHandler, 0),
config: config,
logger: logger,
patternDetector: NewSuspiciousPatternDetector(),
}
// Start cleanup routine
go sm.startCleanupRoutine()
return sm
}
// NewSuspiciousPatternDetector creates a new pattern detector
func NewSuspiciousPatternDetector() *SuspiciousPatternDetector {
return &SuspiciousPatternDetector{
shortWindow: 1 * time.Minute,
mediumWindow: 5 * time.Minute,
longWindow: 15 * time.Minute,
rapidFailureThreshold: 5,
distributedAttackThreshold: 20,
persistentAttackThreshold: 50,
recentEvents: make([]SecurityEvent, 0),
}
}
// RecordAuthenticationFailure records an authentication failure event
func (sm *SecurityMonitor) RecordAuthenticationFailure(clientIP, userAgent, requestPath, reason string, details map[string]any) {
atomic.AddInt64(&sm.authFailures, 1)
event := SecurityEvent{
Type: "authentication_failure",
Severity: "medium",
Timestamp: time.Now(),
ClientIP: clientIP,
UserAgent: userAgent,
RequestPath: requestPath,
Message: fmt.Sprintf("Authentication failed: %s", reason),
Details: details,
}
sm.recordIPFailure(clientIP, "auth_failure")
sm.processSecurityEvent(event)
}
// RecordTokenValidationFailure records a token validation failure
func (sm *SecurityMonitor) RecordTokenValidationFailure(clientIP, userAgent, requestPath, reason string, tokenPrefix string) {
atomic.AddInt64(&sm.tokenValidationFails, 1)
details := map[string]any{
"reason": reason,
}
if tokenPrefix != "" {
details["token_prefix"] = tokenPrefix
}
event := SecurityEvent{
Type: "token_validation_failure",
Severity: "medium",
Timestamp: time.Now(),
ClientIP: clientIP,
UserAgent: userAgent,
RequestPath: requestPath,
Message: fmt.Sprintf("Token validation failed: %s", reason),
Details: details,
}
sm.recordIPFailure(clientIP, "token_failure")
sm.processSecurityEvent(event)
}
// RecordRateLimitHit records when rate limiting is triggered
func (sm *SecurityMonitor) RecordRateLimitHit(clientIP, userAgent, requestPath string) {
atomic.AddInt64(&sm.rateLimitHits, 1)
event := SecurityEvent{
Type: "rate_limit_hit",
Severity: "low",
Timestamp: time.Now(),
ClientIP: clientIP,
UserAgent: userAgent,
RequestPath: requestPath,
Message: "Rate limit exceeded",
Details: map[string]any{
"limit_type": "token_verification",
},
}
sm.recordIPFailure(clientIP, "rate_limit")
sm.processSecurityEvent(event)
}
// RecordSuspiciousActivity records suspicious activity that doesn't fit other categories
func (sm *SecurityMonitor) RecordSuspiciousActivity(clientIP, userAgent, requestPath, activityType, description string, details map[string]any) {
atomic.AddInt64(&sm.suspiciousRequests, 1)
event := SecurityEvent{
Type: "suspicious_activity",
Severity: "high",
Timestamp: time.Now(),
ClientIP: clientIP,
UserAgent: userAgent,
RequestPath: requestPath,
Message: fmt.Sprintf("Suspicious activity detected: %s - %s", activityType, description),
Details: details,
}
sm.recordIPFailure(clientIP, "suspicious")
sm.processSecurityEvent(event)
}
// recordIPFailure tracks failures for a specific IP address
func (sm *SecurityMonitor) recordIPFailure(clientIP, failureType string) {
sm.ipMutex.Lock()
defer sm.ipMutex.Unlock()
tracker, exists := sm.ipFailures[clientIP]
if !exists {
tracker = &IPFailureTracker{
FailureTypes: make(map[string]int64),
FirstFailure: time.Now(),
}
sm.ipFailures[clientIP] = tracker
}
tracker.mutex.Lock()
defer tracker.mutex.Unlock()
tracker.FailureCount++
tracker.LastFailure = time.Now()
tracker.FailureTypes[failureType]++
// Check if IP should be blocked
windowStart := time.Now().Add(-time.Duration(sm.config.FailureWindowMinutes) * time.Minute)
if tracker.FirstFailure.After(windowStart) && tracker.FailureCount >= int64(sm.config.MaxFailuresPerIP) {
if !tracker.IsBlocked {
tracker.IsBlocked = true
tracker.BlockedUntil = time.Now().Add(time.Duration(sm.config.BlockDurationMinutes) * time.Minute)
sm.logger.Errorf("IP %s blocked due to %d failures (types: %v)", clientIP, tracker.FailureCount, tracker.FailureTypes)
// Record blocking event
blockEvent := SecurityEvent{
Type: "ip_blocked",
Severity: "high",
Timestamp: time.Now(),
ClientIP: clientIP,
Message: fmt.Sprintf("IP blocked due to %d failures in %d minutes", tracker.FailureCount, sm.config.FailureWindowMinutes),
Details: map[string]any{
"failure_count": tracker.FailureCount,
"failure_types": tracker.FailureTypes,
"blocked_until": tracker.BlockedUntil,
},
}
sm.processSecurityEvent(blockEvent)
}
}
}
// IsIPBlocked checks if an IP address is currently blocked
func (sm *SecurityMonitor) IsIPBlocked(clientIP string) bool {
sm.ipMutex.RLock()
defer sm.ipMutex.RUnlock()
tracker, exists := sm.ipFailures[clientIP]
if !exists {
return false
}
tracker.mutex.RLock()
defer tracker.mutex.RUnlock()
if tracker.IsBlocked && time.Now().Before(tracker.BlockedUntil) {
return true
}
// Unblock if time has passed
if tracker.IsBlocked && time.Now().After(tracker.BlockedUntil) {
tracker.IsBlocked = false
sm.logger.Infof("IP %s automatically unblocked", clientIP)
}
return false
}
// processSecurityEvent processes a security event through all handlers and pattern detection
func (sm *SecurityMonitor) processSecurityEvent(event SecurityEvent) {
// Add to pattern detector
if sm.config.EnablePatternDetection {
sm.patternDetector.AddEvent(event)
// Check for suspicious patterns
if patterns := sm.patternDetector.DetectSuspiciousPatterns(); len(patterns) > 0 {
for _, pattern := range patterns {
sm.logger.Errorf("Suspicious pattern detected: %s", pattern)
patternEvent := SecurityEvent{
Type: "suspicious_pattern",
Severity: "high",
Timestamp: time.Now(),
Message: fmt.Sprintf("Suspicious pattern detected: %s", pattern),
Details: map[string]any{
"pattern_type": pattern,
"trigger_event": event,
},
}
sm.handleSecurityEvent(patternEvent)
}
}
}
sm.handleSecurityEvent(event)
}
// handleSecurityEvent sends the event to all registered handlers
func (sm *SecurityMonitor) handleSecurityEvent(event SecurityEvent) {
// Log the event
if sm.config.EnableDetailedLogging && (!sm.config.LogSuspiciousOnly || event.Severity == "high") {
sm.logger.Infof("Security Event [%s/%s]: %s (IP: %s, Path: %s)",
event.Type, event.Severity, event.Message, event.ClientIP, event.RequestPath)
}
// Send to all handlers
for _, handler := range sm.eventHandlers {
go handler.HandleSecurityEvent(event)
}
}
// AddEventHandler adds a security event handler
func (sm *SecurityMonitor) AddEventHandler(handler SecurityEventHandler) {
sm.eventHandlers = append(sm.eventHandlers, handler)
}
// GetSecurityMetrics returns current security metrics
func (sm *SecurityMonitor) GetSecurityMetrics() map[string]any {
sm.ipMutex.RLock()
defer sm.ipMutex.RUnlock()
blockedIPs := 0
totalTrackedIPs := len(sm.ipFailures)
for _, tracker := range sm.ipFailures {
tracker.mutex.RLock()
if tracker.IsBlocked && time.Now().Before(tracker.BlockedUntil) {
blockedIPs++
}
tracker.mutex.RUnlock()
}
return map[string]any{
"auth_failures": atomic.LoadInt64(&sm.authFailures),
"token_validation_fails": atomic.LoadInt64(&sm.tokenValidationFails),
"rate_limit_hits": atomic.LoadInt64(&sm.rateLimitHits),
"suspicious_requests": atomic.LoadInt64(&sm.suspiciousRequests),
"blocked_ips": blockedIPs,
"tracked_ips": totalTrackedIPs,
"uptime_hours": time.Since(time.Now().Add(-24 * time.Hour)).Hours(), // Placeholder
}
}
// AddEvent adds an event to the pattern detector
func (spd *SuspiciousPatternDetector) AddEvent(event SecurityEvent) {
spd.eventsMutex.Lock()
defer spd.eventsMutex.Unlock()
spd.recentEvents = append(spd.recentEvents, event)
// Clean old events
cutoff := time.Now().Add(-spd.longWindow)
var filteredEvents []SecurityEvent
for _, e := range spd.recentEvents {
if e.Timestamp.After(cutoff) {
filteredEvents = append(filteredEvents, e)
}
}
spd.recentEvents = filteredEvents
}
// DetectSuspiciousPatterns analyzes recent events for suspicious patterns
func (spd *SuspiciousPatternDetector) DetectSuspiciousPatterns() []string {
spd.eventsMutex.RLock()
defer spd.eventsMutex.RUnlock()
var patterns []string
now := time.Now()
// Check for rapid failures from single IP
ipCounts := make(map[string]int)
shortWindowStart := now.Add(-spd.shortWindow)
for _, event := range spd.recentEvents {
if event.Timestamp.After(shortWindowStart) &&
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
ipCounts[event.ClientIP]++
}
}
for ip, count := range ipCounts {
if count >= spd.rapidFailureThreshold {
patterns = append(patterns, fmt.Sprintf("rapid_failures_from_ip_%s", ip))
}
}
// Check for distributed attack (many IPs failing)
mediumWindowStart := now.Add(-spd.mediumWindow)
uniqueFailingIPs := make(map[string]bool)
for _, event := range spd.recentEvents {
if event.Timestamp.After(mediumWindowStart) &&
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
uniqueFailingIPs[event.ClientIP] = true
}
}
if len(uniqueFailingIPs) >= spd.distributedAttackThreshold {
patterns = append(patterns, "distributed_attack_pattern")
}
// Check for persistent attack
longWindowStart := now.Add(-spd.longWindow)
persistentFailures := 0
for _, event := range spd.recentEvents {
if event.Timestamp.After(longWindowStart) &&
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
persistentFailures++
}
}
if persistentFailures >= spd.persistentAttackThreshold {
patterns = append(patterns, "persistent_attack_pattern")
}
return patterns
}
// startCleanupRoutine starts the background cleanup routine
func (sm *SecurityMonitor) startCleanupRoutine() {
ticker := time.NewTicker(time.Duration(sm.config.CleanupIntervalMinutes) * time.Minute)
defer ticker.Stop()
for range ticker.C {
sm.cleanup()
}
}
// cleanup removes old tracking data
func (sm *SecurityMonitor) cleanup() {
sm.ipMutex.Lock()
defer sm.ipMutex.Unlock()
cutoff := time.Now().Add(-time.Duration(sm.config.RetentionHours) * time.Hour)
for ip, tracker := range sm.ipFailures {
tracker.mutex.RLock()
shouldRemove := tracker.LastFailure.Before(cutoff) && !tracker.IsBlocked
tracker.mutex.RUnlock()
if shouldRemove {
delete(sm.ipFailures, ip)
}
}
sm.logger.Debugf("Security monitor cleanup completed, tracking %d IPs", len(sm.ipFailures))
}
// ExtractClientIP extracts the client IP from the request, considering proxy headers
func ExtractClientIP(r *http.Request) string {
// Check X-Real-IP header first (highest priority)
if xri := r.Header.Get("X-Real-IP"); xri != "" {
if net.ParseIP(xri) != nil {
return xri
}
}
// Check X-Forwarded-For header second
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// Take the first IP in the chain
ips := strings.Split(xff, ",")
if len(ips) > 0 {
ip := strings.TrimSpace(ips[0])
if net.ParseIP(ip) != nil {
return ip
}
}
}
// Fall back to RemoteAddr
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
// LoggingSecurityEventHandler logs security events to the standard logger
type LoggingSecurityEventHandler struct {
logger *Logger
}
// NewLoggingSecurityEventHandler creates a new logging event handler
func NewLoggingSecurityEventHandler(logger *Logger) *LoggingSecurityEventHandler {
return &LoggingSecurityEventHandler{logger: logger}
}
// HandleSecurityEvent implements SecurityEventHandler
func (h *LoggingSecurityEventHandler) HandleSecurityEvent(event SecurityEvent) {
switch event.Severity {
case "high":
h.logger.Errorf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
case "medium":
h.logger.Errorf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
case "low":
h.logger.Infof("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
default:
h.logger.Debugf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
}
}
// MetricsSecurityEventHandler tracks security metrics
type MetricsSecurityEventHandler struct {
eventCounts map[string]int64
mutex sync.RWMutex
}
// NewMetricsSecurityEventHandler creates a new metrics event handler
func NewMetricsSecurityEventHandler() *MetricsSecurityEventHandler {
return &MetricsSecurityEventHandler{
eventCounts: make(map[string]int64),
}
}
// HandleSecurityEvent implements SecurityEventHandler
func (h *MetricsSecurityEventHandler) HandleSecurityEvent(event SecurityEvent) {
h.mutex.Lock()
defer h.mutex.Unlock()
h.eventCounts[event.Type]++
h.eventCounts[fmt.Sprintf("%s_%s", event.Type, event.Severity)]++
}
// GetMetrics returns the current metrics
func (h *MetricsSecurityEventHandler) GetMetrics() map[string]int64 {
h.mutex.RLock()
defer h.mutex.RUnlock()
metrics := make(map[string]int64)
maps.Copy(metrics, h.eventCounts)
return metrics
}