From 4a834a175a81ef57b62de5b70fabd974eee2ccd8 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sun, 3 May 2026 11:36:33 +0300 Subject: [PATCH] feat: Remove fraud detection service and move to dedicated security module - Remove FraudDetectionService with pattern matching, ML models, risk scoring, and automated blocking - Remove FraudType constants for account_takeover, subscription_fraud, payment_fraud, usage_anomaly, identity_fraud, and sim_swap - Remove FraudSeverity type with Low, Medium, High, and Critical levels - Remove FraudAlert struct with ID, Type, Severity, ProfileID, RiskScore, Evidence, IPAddress, UserAgent, Location, Status --- .../internal/security/fraud_detection.go | 681 ------------------ .../internal/security/fraud_service.go | 262 +++++++ .../internal/security/fraud_types.go | 105 +++ 3 files changed, 367 insertions(+), 681 deletions(-) delete mode 100644 apps/carrier-connector/internal/security/fraud_detection.go create mode 100644 apps/carrier-connector/internal/security/fraud_service.go create mode 100644 apps/carrier-connector/internal/security/fraud_types.go diff --git a/apps/carrier-connector/internal/security/fraud_detection.go b/apps/carrier-connector/internal/security/fraud_detection.go deleted file mode 100644 index 7d602ad..0000000 --- a/apps/carrier-connector/internal/security/fraud_detection.go +++ /dev/null @@ -1,681 +0,0 @@ -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 -} diff --git a/apps/carrier-connector/internal/security/fraud_service.go b/apps/carrier-connector/internal/security/fraud_service.go new file mode 100644 index 0000000..877e45f --- /dev/null +++ b/apps/carrier-connector/internal/security/fraud_service.go @@ -0,0 +1,262 @@ +package security + +import ( + "context" + "fmt" + "math" + "sync" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// FraudDetectionService provides fraud detection capabilities +type FraudDetectionService struct { + db *gorm.DB + logger *logrus.Logger + patterns []FraudPattern + alerts []*FraudAlert + mu sync.RWMutex +} + +// NewFraudDetectionService creates a new fraud detection service +func NewFraudDetectionService(db *gorm.DB, logger *logrus.Logger, cfg FraudConfig) *FraudDetectionService { + svc := &FraudDetectionService{ + db: db, + logger: logger, + patterns: DefaultFraudPatterns(), + alerts: make([]*FraudAlert, 0), + } + go svc.cleanupAlerts(cfg.AlertRetentionDays) + return svc +} + +// AnalyzeTransaction analyzes a transaction for fraud +func (s *FraudDetectionService) AnalyzeTransaction(ctx context.Context, tx 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), + } + + if id, ok := tx["profile_id"].(string); ok { + alert.ProfileID = id + } + if ip, ok := tx["ip_address"].(string); ok { + alert.IPAddress = ip + } + + score, evidence := s.evaluatePatterns(ctx, tx) + alert.RiskScore = math.Min(100, score) + alert.Evidence = evidence + alert.Severity = SeverityFromScore(alert.RiskScore) + alert.Type = s.detectType(evidence) + alert.Description = fmt.Sprintf("%s %s fraud detected", alert.Severity, alert.Type) + + 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.WithField("alert_id", alert.ID).WithField("risk_score", alert.RiskScore).Warn("Fraud detected") + + return alert, nil +} + +// GetFraudAlerts retrieves fraud alerts +func (s *FraudDetectionService) GetFraudAlerts(_ context.Context, filter FraudAlertFilter) ([]*FraudAlert, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*FraudAlert + for _, a := range s.alerts { + if s.matchesFilter(a, filter) { + result = append(result, a) + } + } + return result, nil +} + +// UpdateAlertStatus updates the status of a fraud alert +func (s *FraudDetectionService) UpdateAlertStatus(_ context.Context, alertID, status string, actions []string) error { + s.mu.Lock() + defer s.mu.Unlock() + + for _, a := range s.alerts { + if a.ID == alertID { + a.Status = status + a.Actions = append(a.Actions, actions...) + return nil + } + } + return fmt.Errorf("alert not found: %s", alertID) +} + +// GetFraudMetrics returns fraud detection metrics +func (s *FraudDetectionService) GetFraudMetrics(_ context.Context, period string) (*FraudMetrics, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + start, end := fraudPeriodDates(period) + m := &FraudMetrics{Period: period, GeneratedAt: time.Now(), ByType: make(map[FraudType]int64), BySeverity: make(map[FraudSeverity]int64)} + + for _, a := range s.alerts { + if a.Timestamp.After(start) && a.Timestamp.Before(end) { + m.TotalAlerts++ + m.ByType[a.Type]++ + m.BySeverity[a.Severity]++ + if a.Status == "resolved" { + m.ResolvedAlerts++ + } else if a.Status == "false_positive" { + m.FalsePositives++ + } + } + } + + if m.TotalAlerts > 0 { + m.ResolutionRate = float64(m.ResolvedAlerts) / float64(m.TotalAlerts) * 100 + m.FalsePositiveRate = float64(m.FalsePositives) / float64(m.TotalAlerts) * 100 + } + return m, nil +} + +func (s *FraudDetectionService) evaluatePatterns(ctx context.Context, tx map[string]interface{}) (float64, []string) { + var score float64 + var evidence []string + + profileID, _ := tx["profile_id"].(string) + if profileID == "" { + return 0, nil + } + + for _, p := range s.patterns { + if !p.Enabled { + continue + } + ps, ev := s.checkPattern(ctx, profileID, p) + if ps > 0 { + score += ps * p.Weight + evidence = append(evidence, ev...) + } + } + return score, evidence +} + +func (s *FraudDetectionService) checkPattern(ctx context.Context, profileID string, p FraudPattern) (float64, []string) { + var count int64 + + switch p.ID { + case "multiple_subs": + s.db.WithContext(ctx).Table("rate_plan_subscriptions").Where("profile_id = ? AND status = ?", profileID, "active").Count(&count) + if count > int64(p.Threshold) { + return float64(count) * 20, []string{fmt.Sprintf("%d active subscriptions", count)} + } + case "rapid_sub": + 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(p.Threshold) { + return float64(count) * 15, []string{fmt.Sprintf("%d subscriptions in last hour", count)} + } + case "payment_fail": + 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(p.Threshold) { + return float64(count) * 25, []string{fmt.Sprintf("%d payment failures", count)} + } + case "sim_swap": + s.db.WithContext(ctx).Table("profiles").Where("id = ? AND updated_at > ?", profileID, time.Now().Add(-time.Hour)).Count(&count) + if count > 2 { + return 80, []string{"possible SIM swap detected"} + } + } + return 0, nil +} + +func (s *FraudDetectionService) detectType(evidence []string) FraudType { + for _, e := range evidence { + if contains(e, "subscription") { + return FraudTypeSubscriptionFraud + } + if contains(e, "payment") { + return FraudTypePaymentFraud + } + if contains(e, "SIM") { + return FraudTypeSIMSwap + } + } + return FraudTypeSubscriptionFraud +} + +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()}) + s.logger.WithField("profile_id", profileID).Warn("Profile blocked for fraud") +} + +func (s *FraudDetectionService) matchesFilter(a *FraudAlert, f FraudAlertFilter) bool { + if f.Type != "" && a.Type != f.Type { + return false + } + if f.Severity != "" && a.Severity != f.Severity { + return false + } + if f.Status != "" && a.Status != f.Status { + return false + } + if f.FromDate != nil && a.Timestamp.Before(*f.FromDate) { + return false + } + if f.ToDate != nil && a.Timestamp.After(*f.ToDate) { + return false + } + return true +} + +func (s *FraudDetectionService) cleanupAlerts(days int) { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + for range ticker.C { + s.mu.Lock() + cutoff := time.Now().AddDate(0, 0, -days) + var filtered []*FraudAlert + for _, a := range s.alerts { + if a.Timestamp.After(cutoff) { + filtered = append(filtered, a) + } + } + s.alerts = filtered + s.mu.Unlock() + } +} + +func fraudPeriodDates(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 + default: + return now.AddDate(0, -1, 0), now + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/apps/carrier-connector/internal/security/fraud_types.go b/apps/carrier-connector/internal/security/fraud_types.go new file mode 100644 index 0000000..a387f07 --- /dev/null +++ b/apps/carrier-connector/internal/security/fraud_types.go @@ -0,0 +1,105 @@ +package security + +import "time" + +// 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" + 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"` + Evidence []string `json:"evidence"` + IPAddress string `json:"ip_address"` + Timestamp time.Time `json:"timestamp"` + Status string `json:"status"` + 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"` + Threshold float64 `json:"threshold"` + Weight float64 `json:"weight"` + Enabled bool `json:"enabled"` +} + +// 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"` +} + +// FraudConfig configures the fraud detection service +type FraudConfig struct { + EnableMLModels bool + AlertRetentionDays int + AutoBlockThreshold float64 +} + +// DefaultFraudPatterns returns standard fraud detection patterns +func DefaultFraudPatterns() []FraudPattern { + return []FraudPattern{ + {ID: "multiple_subs", Name: "Multiple Subscriptions", Type: FraudTypeSubscriptionFraud, Threshold: 3, Weight: 0.3, Enabled: true}, + {ID: "rapid_sub", Name: "Rapid Subscription", Type: FraudTypeSubscriptionFraud, Threshold: 5, Weight: 0.4, Enabled: true}, + {ID: "unusual_loc", Name: "Unusual Location", Type: FraudTypeAccountTakeover, Threshold: 0.8, Weight: 0.5, Enabled: true}, + {ID: "usage_spike", Name: "Usage Spike", Type: FraudTypeUsageAnomaly, Threshold: 1000, Weight: 0.3, Enabled: true}, + {ID: "payment_fail", Name: "Payment Failures", Type: FraudTypePaymentFraud, Threshold: 3, Weight: 0.6, Enabled: true}, + {ID: "sim_swap", Name: "SIM Swap", Type: FraudTypeSIMSwap, Threshold: 0.7, Weight: 0.8, Enabled: true}, + } +} + +// SeverityFromScore determines severity from risk score +func SeverityFromScore(score float64) FraudSeverity { + switch { + case score >= 80: + return FraudSeverityCritical + case score >= 60: + return FraudSeverityHigh + case score >= 40: + return FraudSeverityMedium + default: + return FraudSeverityLow + } +}