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
This commit is contained in:
nutcas3
2026-05-03 11:36:33 +03:00
parent 06661ca0ad
commit 4a834a175a
3 changed files with 367 additions and 681 deletions
@@ -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(&currentUsage)
// 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
}
@@ -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
}
@@ -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
}
}