diff --git a/apps/carrier-connector/internal/security/fraud_detection.go b/apps/carrier-connector/internal/security/fraud_detection.go new file mode 100644 index 0000000..7d602ad --- /dev/null +++ b/apps/carrier-connector/internal/security/fraud_detection.go @@ -0,0 +1,681 @@ +package security + +import ( + "context" + "fmt" + "math" + "sync" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// FraudType represents different types of fraud +type FraudType string + +const ( + FraudTypeAccountTakeover FraudType = "account_takeover" + FraudTypeSubscriptionFraud FraudType = "subscription_fraud" + FraudTypePaymentFraud FraudType = "payment_fraud" + FraudTypeUsageAnomaly FraudType = "usage_anomaly" + FraudTypeIdentityFraud FraudType = "identity_fraud" + FraudTypeSIMSwap FraudType = "sim_swap" +) + +// FraudSeverity represents the severity of fraud detection +type FraudSeverity string + +const ( + FraudSeverityLow FraudSeverity = "low" + FraudSeverityMedium FraudSeverity = "medium" + FraudSeverityHigh FraudSeverity = "high" + FraudSeverityCritical FraudSeverity = "critical" +) + +// FraudAlert represents a fraud detection alert +type FraudAlert struct { + ID string `json:"id"` + Type FraudType `json:"type"` + Severity FraudSeverity `json:"severity"` + ProfileID string `json:"profile_id"` + Description string `json:"description"` + RiskScore float64 `json:"risk_score"` // 0-100 + Evidence []string `json:"evidence"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Location string `json:"location"` + Timestamp time.Time `json:"timestamp"` + Status string `json:"status"` // "new", "investigating", "resolved", "false_positive" + Actions []string `json:"actions_taken"` + Metadata map[string]any `json:"metadata"` +} + +// FraudPattern represents a fraud detection pattern +type FraudPattern struct { + ID string `json:"id"` + Name string `json:"name"` + Type FraudType `json:"type"` + Description string `json:"description"` + Threshold float64 `json:"threshold"` + Weight float64 `json:"weight"` + Enabled bool `json:"enabled"` +} + +// FraudDetectionService provides fraud detection capabilities +type FraudDetectionService struct { + db *gorm.DB + logger *logrus.Logger + patterns []FraudPattern + alerts []*FraudAlert + mu sync.RWMutex + riskModels map[string]*RiskModel +} + +// RiskModel represents a machine learning model for fraud detection +type RiskModel struct { + Name string `json:"name"` + Type FraudType `json:"type"` + Version string `json:"version"` + LastTrained time.Time `json:"last_trained"` + Accuracy float64 `json:"accuracy"` + Threshold float64 `json:"threshold"` +} + +// FraudDetectionConfig configures the fraud detection service +type FraudDetectionConfig struct { + EnableMLModels bool + RiskThreshold float64 + AlertRetention int // days + AutoBlockThreshold float64 +} + +// NewFraudDetectionService creates a new fraud detection service +func NewFraudDetectionService(db *gorm.DB, logger *logrus.Logger, config FraudDetectionConfig) *FraudDetectionService { + service := &FraudDetectionService{ + db: db, + logger: logger, + patterns: getDefaultFraudPatterns(), + alerts: make([]*FraudAlert, 0), + riskModels: make(map[string]*RiskModel), + } + + // Initialize ML models if enabled + if config.EnableMLModels { + service.initializeRiskModels() + } + + go service.cleanupOldAlerts(config.AlertRetention) + + return service +} + +// AnalyzeTransaction analyzes a transaction for fraud +func (s *FraudDetectionService) AnalyzeTransaction(ctx context.Context, transaction map[string]interface{}) (*FraudAlert, error) { + s.mu.Lock() + defer s.mu.Unlock() + + alert := &FraudAlert{ + ID: fmt.Sprintf("fraud-%d", time.Now().UnixNano()), + Timestamp: time.Now(), + Status: "new", + Metadata: make(map[string]any), + } + + // Extract transaction details + if profileID, ok := transaction["profile_id"].(string); ok { + alert.ProfileID = profileID + } + if ip, ok := transaction["ip_address"].(string); ok { + alert.IPAddress = ip + } + if ua, ok := transaction["user_agent"].(string); ok { + alert.UserAgent = ua + } + + // Run fraud detection patterns + riskScore := 0.0 + evidence := make([]string, 0) + + for _, pattern := range s.patterns { + if !pattern.Enabled { + continue + } + + patternScore, patternEvidence := s.evaluatePattern(ctx, transaction, pattern) + if patternScore > 0 { + riskScore += patternScore * pattern.Weight + evidence = append(evidence, patternEvidence...) + } + } + + // Apply ML models if available + if mlScore := s.applyRiskModels(ctx, transaction); mlScore > 0 { + riskScore = (riskScore + mlScore) / 2 + evidence = append(evidence, "ML model anomaly detected") + } + + alert.RiskScore = math.Min(100, riskScore) + alert.Evidence = evidence + + // Determine severity and type + alert.Severity = s.determineSeverity(alert.RiskScore) + alert.Type = s.determineFraudType(evidence) + alert.Description = s.generateDescription(alert.Type, alert.Severity, evidence) + + // Take automated actions if needed + if alert.RiskScore >= 80 { + alert.Actions = append(alert.Actions, "auto_blocked") + s.blockProfile(ctx, alert.ProfileID) + } else if alert.RiskScore >= 60 { + alert.Actions = append(alert.Actions, "flagged_for_review") + } + + s.alerts = append(s.alerts, alert) + + s.logger.WithFields(logrus.Fields{ + "alert_id": alert.ID, + "risk_score": alert.RiskScore, + "type": alert.Type, + "severity": alert.Severity, + }).Warn("Fraud detected") + + return alert, nil +} + +// GetFraudAlerts retrieves fraud alerts +func (s *FraudDetectionService) GetFraudAlerts(ctx context.Context, filter FraudAlertFilter) ([]*FraudAlert, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + filtered := make([]*FraudAlert, 0) + + for _, alert := range s.alerts { + if s.matchesFilter(alert, filter) { + filtered = append(filtered, alert) + } + } + + return filtered, nil +} + +// UpdateAlertStatus updates the status of a fraud alert +func (s *FraudDetectionService) UpdateAlertStatus(ctx context.Context, alertID, status string, actions []string) error { + s.mu.Lock() + defer s.mu.Unlock() + + for _, alert := range s.alerts { + if alert.ID == alertID { + alert.Status = status + if len(actions) > 0 { + alert.Actions = append(alert.Actions, actions...) + } + return nil + } + } + + return fmt.Errorf("alert not found: %s", alertID) +} + +// GetFraudMetrics returns fraud detection metrics +func (s *FraudDetectionService) GetFraudMetrics(ctx context.Context, period string) (*FraudMetrics, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + metrics := &FraudMetrics{ + Period: period, + GeneratedAt: time.Now(), + ByType: make(map[FraudType]int64), + BySeverity: make(map[FraudSeverity]int64), + } + + startDate, endDate := s.getPeriodDates(period) + + for _, alert := range s.alerts { + if alert.Timestamp.After(startDate) && alert.Timestamp.Before(endDate) { + metrics.TotalAlerts++ + metrics.ByType[alert.Type]++ + metrics.BySeverity[alert.Severity]++ + + if alert.Status == "resolved" { + metrics.ResolvedAlerts++ + } else if alert.Status == "false_positive" { + metrics.FalsePositives++ + } + } + } + + // Calculate rates + if metrics.TotalAlerts > 0 { + metrics.ResolutionRate = float64(metrics.ResolvedAlerts) / float64(metrics.TotalAlerts) * 100 + metrics.FalsePositiveRate = float64(metrics.FalsePositives) / float64(metrics.TotalAlerts) * 100 + } + + return metrics, nil +} + +// FraudMetrics represents fraud detection metrics +type FraudMetrics struct { + Period string `json:"period"` + TotalAlerts int64 `json:"total_alerts"` + ResolvedAlerts int64 `json:"resolved_alerts"` + FalsePositives int64 `json:"false_positives"` + ResolutionRate float64 `json:"resolution_rate_pct"` + FalsePositiveRate float64 `json:"false_positive_rate_pct"` + ByType map[FraudType]int64 `json:"by_type"` + BySeverity map[FraudSeverity]int64 `json:"by_severity"` + GeneratedAt time.Time `json:"generated_at"` +} + +// FraudAlertFilter filters fraud alerts +type FraudAlertFilter struct { + Type FraudType `json:"type,omitempty"` + Severity FraudSeverity `json:"severity,omitempty"` + Status string `json:"status,omitempty"` + FromDate *time.Time `json:"from_date,omitempty"` + ToDate *time.Time `json:"to_date,omitempty"` + Limit int `json:"limit,omitempty"` +} + +// getDefaultFraudPatterns returns default fraud detection patterns +func getDefaultFraudPatterns() []FraudPattern { + return []FraudPattern{ + { + ID: "multiple_subscriptions", + Name: "Multiple Subscriptions", + Type: FraudTypeSubscriptionFraud, + Description: "Multiple active subscriptions from same profile", + Threshold: 3, + Weight: 0.3, + Enabled: true, + }, + { + ID: "rapid_subscription", + Name: "Rapid Subscription Creation", + Type: FraudTypeSubscriptionFraud, + Description: "Multiple subscriptions created in short time", + Threshold: 5, + Weight: 0.4, + Enabled: true, + }, + { + ID: "unusual_location", + Name: "Unusual Location Access", + Type: FraudTypeAccountTakeover, + Description: "Access from unusual geographic location", + Threshold: 0.8, + Weight: 0.5, + Enabled: true, + }, + { + ID: "high_usage_spike", + Name: "High Usage Spike", + Type: FraudTypeUsageAnomaly, + Description: "Sudden spike in data usage", + Threshold: 1000, // MB + Weight: 0.3, + Enabled: true, + }, + { + ID: "payment_failure_pattern", + Name: "Payment Failure Pattern", + Type: FraudTypePaymentFraud, + Description: "Multiple payment failures", + Threshold: 3, + Weight: 0.6, + Enabled: true, + }, + { + ID: "sim_swap_indication", + Name: "SIM Swap Indication", + Type: FraudTypeSIMSwap, + Description: "Indicators of SIM swap attack", + Threshold: 0.7, + Weight: 0.8, + Enabled: true, + }, + } +} + +// evaluatePattern evaluates a specific fraud pattern +func (s *FraudDetectionService) evaluatePattern(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + evidence := make([]string, 0) + score := 0.0 + + switch pattern.ID { + case "multiple_subscriptions": + score, evidence = s.checkMultipleSubscriptions(ctx, transaction, pattern) + case "rapid_subscription": + score, evidence = s.checkRapidSubscription(ctx, transaction, pattern) + case "unusual_location": + score, evidence = s.checkUnusualLocation(ctx, transaction, pattern) + case "high_usage_spike": + score, evidence = s.checkUsageSpike(ctx, transaction, pattern) + case "payment_failure_pattern": + score, evidence = s.checkPaymentFailures(ctx, transaction, pattern) + case "sim_swap_indication": + score, evidence = s.checkSIMSwap(ctx, transaction, pattern) + } + + return score, evidence +} + +// checkMultipleSubscriptions checks for multiple subscriptions +func (s *FraudDetectionService) checkMultipleSubscriptions(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + var count int64 + s.db.WithContext(ctx).Table("rate_plan_subscriptions"). + Where("profile_id = ? AND status = ?", profileID, "active"). + Count(&count) + + if count > int64(pattern.Threshold) { + return float64(count) * 20, []string{fmt.Sprintf("Found %d active subscriptions", count)} + } + + return 0, nil +} + +// checkRapidSubscription checks for rapid subscription creation +func (s *FraudDetectionService) checkRapidSubscription(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + var count int64 + s.db.WithContext(ctx).Table("rate_plan_subscriptions"). + Where("profile_id = ? AND created_at > ?", profileID, time.Now().Add(-time.Hour)). + Count(&count) + + if count > int64(pattern.Threshold) { + return float64(count) * 15, []string{fmt.Sprintf("Created %d subscriptions in last hour", count)} + } + + return 0, nil +} + +// checkUnusualLocation checks for unusual geographic access +func (s *FraudDetectionService) checkUnusualLocation(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + ipAddress, ok := transaction["ip_address"].(string) + if !ok { + return 0, nil + } + + // Simplified location check - in production use GeoIP + country := s.getCountryFromIP(ipAddress) + + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + // Check if this is a new country for this profile + var prevCountry string + s.db.WithContext(ctx).Table("profiles"). + Where("id = ?", profileID). + Select("country"). + Scan(&prevCountry) + + if prevCountry != "" && country != prevCountry { + return 70, []string{fmt.Sprintf("Access from new country: %s (previous: %s)", country, prevCountry)} + } + + return 0, nil +} + +// checkUsageSpike checks for unusual usage patterns +func (s *FraudDetectionService) checkUsageSpike(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + // Get current usage + var currentUsage int64 + s.db.WithContext(ctx).Table("rate_plan_usage"). + Where("profile_id = ? AND created_at > ?", profileID, time.Now().Add(-24*time.Hour)). + Select("COALESCE(SUM(data_used), 0)"). + Scan(¤tUsage) + + // Get average usage for comparison + var avgUsage int64 + s.db.WithContext(ctx).Table("rate_plan_usage"). + Where("profile_id = ? AND created_at BETWEEN ? AND ?", + profileID, time.Now().Add(-30*24*time.Hour), time.Now().Add(-24*time.Hour)). + Select("COALESCE(AVG(data_used), 0)"). + Scan(&avgUsage) + + if avgUsage > 0 && currentUsage > avgUsage*5 && currentUsage > int64(pattern.Threshold) { + return 60, []string{fmt.Sprintf("Usage spike: %d MB (avg: %d MB)", currentUsage, avgUsage)} + } + + return 0, nil +} + +// checkPaymentFailures checks for payment failure patterns +func (s *FraudDetectionService) checkPaymentFailures(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + var count int64 + s.db.WithContext(ctx).Table("billing_transactions"). + Where("profile_id = ? AND status = ? AND created_at > ?", profileID, "failed", time.Now().Add(-24*time.Hour)). + Count(&count) + + if count > int64(pattern.Threshold) { + return float64(count) * 25, []string{fmt.Sprintf("%d payment failures in last 24 hours", count)} + } + + return 0, nil +} + +// checkSIMSwap checks for SIM swap indicators +func (s *FraudDetectionService) checkSIMSwap(ctx context.Context, transaction map[string]interface{}, pattern FraudPattern) (float64, []string) { + // Check for multiple profile changes in short time + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0, nil + } + + var updates int64 + s.db.WithContext(ctx).Table("profiles"). + Where("id = ? AND updated_at > ?", profileID, time.Now().Add(-time.Hour)). + Count(&updates) + + if updates > 2 { + return 80, []string{fmt.Sprintf("%d profile updates in last hour (possible SIM swap)", updates)} + } + + return 0, nil +} + +// getCountryFromIP simulates GeoIP lookup +func (s *FraudDetectionService) getCountryFromIP(ip string) string { + // Simplified - in production use actual GeoIP database + if ip == "127.0.0.1" || ip == "::1" { + return "US" + } + return "Unknown" +} + +// determineSeverity determines fraud severity from risk score +func (s *FraudDetectionService) determineSeverity(score float64) FraudSeverity { + switch { + case score >= 80: + return FraudSeverityCritical + case score >= 60: + return FraudSeverityHigh + case score >= 40: + return FraudSeverityMedium + default: + return FraudSeverityLow + } +} + +// determineFraudType determines fraud type from evidence +func (s *FraudDetectionService) determineFraudType(evidence []string) FraudType { + for _, ev := range evidence { + if containsIgnoreCaseFraud(ev, "subscription") { + return FraudTypeSubscriptionFraud + } + if containsIgnoreCaseFraud(ev, "payment") { + return FraudTypePaymentFraud + } + if containsIgnoreCaseFraud(ev, "location") || containsIgnoreCaseFraud(ev, "country") { + return FraudTypeAccountTakeover + } + if containsIgnoreCaseFraud(ev, "usage") { + return FraudTypeUsageAnomaly + } + if containsIgnoreCaseFraud(ev, "sim swap") { + return FraudTypeSIMSwap + } + } + return FraudTypeSubscriptionFraud // Default +} + +// ... +// generateDescription generates alert description +func (s *FraudDetectionService) generateDescription(fraudType FraudType, severity FraudSeverity, evidence []string) string { + desc := fmt.Sprintf("%s %s fraud detected", string(severity), string(fraudType)) + if len(evidence) > 0 { + desc += fmt.Sprintf(": %s", evidence[0]) + } + return desc +} + +// blockProfile blocks a profile for fraud +func (s *FraudDetectionService) blockProfile(ctx context.Context, profileID string) { + s.db.WithContext(ctx).Table("profiles"). + Where("id = ?", profileID). + Updates(map[string]interface{}{ + "status": "blocked", + "blocked_at": time.Now(), + "block_reason": "fraud_detection", + }) + + s.logger.WithField("profile_id", profileID).Warn("Profile blocked due to fraud detection") +} + +// applyRiskModels applies ML models for fraud detection +func (s *FraudDetectionService) applyRiskModels(ctx context.Context, transaction map[string]interface{}) float64 { + // Simplified ML model application + // In production, this would use actual trained models + profileID, ok := transaction["profile_id"].(string) + if !ok { + return 0 + } + + // Get profile history + var subscriptionCount int64 + s.db.WithContext(ctx).Table("rate_plan_subscriptions"). + Where("profile_id = ?", profileID). + Count(&subscriptionCount) + + var paymentFailures int64 + s.db.WithContext(ctx).Table("billing_transactions"). + Where("profile_id = ? AND status = ?", profileID, "failed"). + Count(&paymentFailures) + + // Simple risk scoring + risk := 0.0 + if subscriptionCount > 5 { + risk += 30 + } + if paymentFailures > 2 { + risk += 40 + } + + return risk +} + +// initializeRiskModels initializes ML risk models +func (s *FraudDetectionService) initializeRiskModels() { + s.riskModels["subscription_fraud"] = &RiskModel{ + Name: "Subscription Fraud Model", + Type: FraudTypeSubscriptionFraud, + Version: "1.0", + LastTrained: time.Now().AddDate(0, -1, 0), + Accuracy: 0.92, + Threshold: 0.75, + } + + s.riskModels["account_takeover"] = &RiskModel{ + Name: "Account Takeover Model", + Type: FraudTypeAccountTakeover, + Version: "1.0", + LastTrained: time.Now().AddDate(0, -1, 0), + Accuracy: 0.88, + Threshold: 0.80, + } +} + +// matchesFilter checks if alert matches filter criteria +func (s *FraudDetectionService) matchesFilter(alert *FraudAlert, filter FraudAlertFilter) bool { + if filter.Type != "" && alert.Type != filter.Type { + return false + } + if filter.Severity != "" && alert.Severity != filter.Severity { + return false + } + if filter.Status != "" && alert.Status != filter.Status { + return false + } + if filter.FromDate != nil && alert.Timestamp.Before(*filter.FromDate) { + return false + } + if filter.ToDate != nil && alert.Timestamp.After(*filter.ToDate) { + return false + } + return true +} + +// getPeriodDates returns start and end dates for a period +func (s *FraudDetectionService) getPeriodDates(period string) (time.Time, time.Time) { + now := time.Now() + + switch period { + case "daily": + return now.Truncate(24 * time.Hour), now + case "weekly": + return now.AddDate(0, 0, -7), now + case "monthly": + return now.AddDate(0, -1, 0), now + case "quarterly": + return now.AddDate(0, -3, 0), now + default: + return now.AddDate(0, -1, 0), now + } +} + +// cleanupOldAlerts removes old alerts +func (s *FraudDetectionService) cleanupOldAlerts(retentionDays int) { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + + for range ticker.C { + s.mu.Lock() + cutoff := time.Now().AddDate(0, 0, -retentionDays) + + filtered := make([]*FraudAlert, 0) + for _, alert := range s.alerts { + if alert.Timestamp.After(cutoff) { + filtered = append(filtered, alert) + } + } + s.alerts = filtered + s.mu.Unlock() + } +} + +// containsIgnoreCaseFraud checks if string contains substring (case insensitive) +func containsIgnoreCaseFraud(s, substr string) bool { + // Simplified case-insensitive check + return len(s) >= len(substr) && s[:len(substr)] == substr +}