mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-14 16:05:13 +00:00
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:
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user