rustkernel_behavioral/
signatures.rs

1//! Fraud signature detection kernels.
2//!
3//! This module provides pattern-based fraud detection using
4//! predefined fraud signatures.
5
6use crate::types::{EventValue, FraudSignature, SignatureMatch, SignaturePattern, UserEvent};
7use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
8use std::collections::HashMap;
9
10// ============================================================================
11// Fraud Signature Detection Kernel
12// ============================================================================
13
14/// Fraud signature detection kernel.
15///
16/// Matches user events against known fraud patterns/signatures.
17#[derive(Debug, Clone)]
18pub struct FraudSignatureDetection {
19    metadata: KernelMetadata,
20}
21
22impl Default for FraudSignatureDetection {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl FraudSignatureDetection {
29    /// Create a new fraud signature detection kernel.
30    #[must_use]
31    pub fn new() -> Self {
32        Self {
33            metadata: KernelMetadata::ring(
34                "behavioral/fraud-signatures",
35                Domain::BehavioralAnalytics,
36            )
37            .with_description("Known fraud pattern signature matching")
38            .with_throughput(150_000)
39            .with_latency_us(30.0),
40        }
41    }
42
43    /// Match events against fraud signatures.
44    ///
45    /// # Arguments
46    /// * `events` - Events to analyze
47    /// * `signatures` - Active fraud signatures to match against
48    pub fn compute(events: &[UserEvent], signatures: &[FraudSignature]) -> Vec<SignatureMatch> {
49        let mut matches = Vec::new();
50
51        for signature in signatures.iter().filter(|s| s.active) {
52            if let Some(match_result) = Self::match_signature(events, signature) {
53                matches.push(match_result);
54            }
55        }
56
57        // Sort by score descending
58        matches.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
59
60        matches
61    }
62
63    /// Match a single signature against events.
64    fn match_signature(events: &[UserEvent], signature: &FraudSignature) -> Option<SignatureMatch> {
65        match &signature.pattern {
66            SignaturePattern::EventSequence(sequence) => {
67                Self::match_event_sequence(events, signature, sequence)
68            }
69            SignaturePattern::EventAttributes(event_type, attrs) => {
70                Self::match_event_attributes(events, signature, event_type, attrs)
71            }
72            SignaturePattern::TimeWindow {
73                events: event_types,
74                window_secs,
75            } => Self::match_time_window(events, signature, event_types, *window_secs),
76            SignaturePattern::CountThreshold {
77                event_type,
78                count,
79                window_secs,
80            } => Self::match_count_threshold(events, signature, event_type, *count, *window_secs),
81            SignaturePattern::Regex(pattern) => Self::match_regex(events, signature, pattern),
82        }
83    }
84
85    /// Match an event sequence pattern.
86    fn match_event_sequence(
87        events: &[UserEvent],
88        signature: &FraudSignature,
89        sequence: &[String],
90    ) -> Option<SignatureMatch> {
91        if sequence.is_empty() || events.len() < sequence.len() {
92            return None;
93        }
94
95        // Sort events by timestamp
96        let mut sorted_events: Vec<_> = events.iter().collect();
97        sorted_events.sort_by_key(|e| e.timestamp);
98
99        // Sliding window to find sequence
100        let mut best_match: Option<(Vec<u64>, f64)> = None;
101
102        for window in sorted_events.windows(sequence.len()) {
103            let mut matched = true;
104            let mut matched_ids = Vec::new();
105
106            for (i, expected_type) in sequence.iter().enumerate() {
107                if window[i].event_type != *expected_type {
108                    matched = false;
109                    break;
110                }
111                matched_ids.push(window[i].id);
112            }
113
114            if matched {
115                // Calculate match score based on time compression
116                let time_span =
117                    window.last().unwrap().timestamp - window.first().unwrap().timestamp;
118                let time_score = if time_span < 300 {
119                    100.0 // Very rapid sequence
120                } else if time_span < 3600 {
121                    80.0
122                } else if time_span < 86400 {
123                    60.0
124                } else {
125                    40.0
126                };
127
128                let score = time_score * (signature.severity / 100.0);
129
130                if best_match.is_none() || score > best_match.as_ref().unwrap().1 {
131                    best_match = Some((matched_ids, score));
132                }
133            }
134        }
135
136        best_match.map(|(matched_events, score)| SignatureMatch {
137            signature_id: signature.id,
138            signature_name: signature.name.clone(),
139            score,
140            matched_events,
141            details: format!("Event sequence matched: {:?}", sequence),
142        })
143    }
144
145    /// Match event with specific attributes.
146    fn match_event_attributes(
147        events: &[UserEvent],
148        signature: &FraudSignature,
149        event_type: &str,
150        required_attrs: &HashMap<String, EventValue>,
151    ) -> Option<SignatureMatch> {
152        let mut matched_events = Vec::new();
153
154        for event in events {
155            if event.event_type != event_type {
156                continue;
157            }
158
159            let attrs_match = required_attrs.iter().all(|(key, expected_value)| {
160                event
161                    .attributes
162                    .get(key)
163                    .is_some_and(|actual| Self::values_match(expected_value, actual))
164            });
165
166            if attrs_match {
167                matched_events.push(event.id);
168            }
169        }
170
171        if matched_events.is_empty() {
172            return None;
173        }
174
175        let score =
176            signature.severity * (matched_events.len() as f64 / events.len() as f64).min(1.0);
177
178        Some(SignatureMatch {
179            signature_id: signature.id,
180            signature_name: signature.name.clone(),
181            score,
182            matched_events,
183            details: format!(
184                "Events of type '{}' matched with required attributes",
185                event_type
186            ),
187        })
188    }
189
190    /// Match events occurring within a time window.
191    fn match_time_window(
192        events: &[UserEvent],
193        signature: &FraudSignature,
194        event_types: &[String],
195        window_secs: u64,
196    ) -> Option<SignatureMatch> {
197        if event_types.is_empty() {
198            return None;
199        }
200
201        // Sort events by timestamp
202        let mut sorted_events: Vec<_> = events.iter().collect();
203        sorted_events.sort_by_key(|e| e.timestamp);
204
205        let mut best_match: Option<(Vec<u64>, f64)> = None;
206
207        // Check each potential window
208        for (i, start_event) in sorted_events.iter().enumerate() {
209            let window_end = start_event.timestamp + window_secs;
210
211            // Collect events in window
212            let window_events: Vec<_> = sorted_events[i..]
213                .iter()
214                .take_while(|e| e.timestamp <= window_end)
215                .collect();
216
217            // Check if all required event types are present
218            let found_types: std::collections::HashSet<_> =
219                window_events.iter().map(|e| &e.event_type).collect();
220
221            let all_present = event_types.iter().all(|t| found_types.contains(t));
222
223            if all_present {
224                let matched_ids: Vec<_> = window_events.iter().map(|e| e.id).collect();
225                let actual_span = window_events.last().unwrap().timestamp
226                    - window_events.first().unwrap().timestamp;
227
228                // Score higher for tighter windows
229                let compression_ratio = 1.0 - (actual_span as f64 / window_secs as f64);
230                let score = signature.severity * (0.5 + compression_ratio * 0.5);
231
232                if best_match.is_none() || score > best_match.as_ref().unwrap().1 {
233                    best_match = Some((matched_ids, score));
234                }
235            }
236        }
237
238        best_match.map(|(matched_events, score)| SignatureMatch {
239            signature_id: signature.id,
240            signature_name: signature.name.clone(),
241            score,
242            matched_events,
243            details: format!(
244                "Events {:?} found within {} second window",
245                event_types, window_secs
246            ),
247        })
248    }
249
250    /// Match count threshold pattern.
251    fn match_count_threshold(
252        events: &[UserEvent],
253        signature: &FraudSignature,
254        event_type: &str,
255        threshold: u32,
256        window_secs: u64,
257    ) -> Option<SignatureMatch> {
258        // Filter relevant events
259        let mut relevant_events: Vec<_> = events
260            .iter()
261            .filter(|e| e.event_type == event_type)
262            .collect();
263
264        if relevant_events.len() < threshold as usize {
265            return None;
266        }
267
268        relevant_events.sort_by_key(|e| e.timestamp);
269
270        let mut best_match: Option<(Vec<u64>, u32, f64)> = None;
271
272        // Sliding window to find threshold breach
273        for (i, start_event) in relevant_events.iter().enumerate() {
274            let window_end = start_event.timestamp + window_secs;
275
276            let window_events: Vec<_> = relevant_events[i..]
277                .iter()
278                .take_while(|e| e.timestamp <= window_end)
279                .collect();
280
281            let count = window_events.len() as u32;
282
283            if count >= threshold {
284                let matched_ids: Vec<_> = window_events.iter().map(|e| e.id).collect();
285
286                // Score based on how much threshold is exceeded
287                let excess_ratio = (count as f64 / threshold as f64) - 1.0;
288                let score = signature.severity * (0.5 + excess_ratio.min(1.0) * 0.5);
289
290                if best_match.is_none() || count > best_match.as_ref().unwrap().1 {
291                    best_match = Some((matched_ids, count, score));
292                }
293            }
294        }
295
296        best_match.map(|(matched_events, count, score)| SignatureMatch {
297            signature_id: signature.id,
298            signature_name: signature.name.clone(),
299            score,
300            matched_events,
301            details: format!(
302                "Count threshold exceeded: {} '{}' events in {} seconds (threshold: {})",
303                count, event_type, window_secs, threshold
304            ),
305        })
306    }
307
308    /// Match regex pattern against event data.
309    fn match_regex(
310        events: &[UserEvent],
311        signature: &FraudSignature,
312        pattern: &str,
313    ) -> Option<SignatureMatch> {
314        // Simple pattern matching (in production, use regex crate)
315        let mut matched_events = Vec::new();
316
317        for event in events {
318            // Check event type
319            if event.event_type.contains(pattern) {
320                matched_events.push(event.id);
321                continue;
322            }
323
324            // Check attributes
325            for value in event.attributes.values() {
326                if Self::value_contains_pattern(value, pattern) {
327                    matched_events.push(event.id);
328                    break;
329                }
330            }
331        }
332
333        if matched_events.is_empty() {
334            return None;
335        }
336
337        let match_count = matched_events.len();
338        let score = signature.severity * (match_count as f64 / events.len() as f64).min(1.0);
339
340        Some(SignatureMatch {
341            signature_id: signature.id,
342            signature_name: signature.name.clone(),
343            score,
344            matched_events,
345            details: format!("Pattern '{}' matched in {} events", pattern, match_count),
346        })
347    }
348
349    /// Check if two EventValues match.
350    fn values_match(expected: &EventValue, actual: &EventValue) -> bool {
351        match (expected, actual) {
352            (EventValue::String(e), EventValue::String(a)) => e == a,
353            (EventValue::Number(e), EventValue::Number(a)) => (e - a).abs() < 0.0001,
354            (EventValue::Bool(e), EventValue::Bool(a)) => e == a,
355            (EventValue::List(e), EventValue::List(a)) => {
356                e.len() == a.len()
357                    && e.iter()
358                        .zip(a.iter())
359                        .all(|(ev, av)| Self::values_match(ev, av))
360            }
361            _ => false,
362        }
363    }
364
365    /// Check if value contains pattern string.
366    fn value_contains_pattern(value: &EventValue, pattern: &str) -> bool {
367        match value {
368            EventValue::String(s) => s.contains(pattern),
369            EventValue::List(items) => items
370                .iter()
371                .any(|v| Self::value_contains_pattern(v, pattern)),
372            _ => false,
373        }
374    }
375
376    /// Get standard fraud signatures.
377    pub fn standard_signatures() -> Vec<FraudSignature> {
378        vec![
379            FraudSignature {
380                id: 1,
381                name: "Rapid Login Attempts".to_string(),
382                pattern: SignaturePattern::CountThreshold {
383                    event_type: "login_attempt".to_string(),
384                    count: 5,
385                    window_secs: 60,
386                },
387                severity: 70.0,
388                active: true,
389            },
390            FraudSignature {
391                id: 2,
392                name: "Account Takeover Sequence".to_string(),
393                pattern: SignaturePattern::EventSequence(vec![
394                    "password_reset".to_string(),
395                    "login".to_string(),
396                    "profile_change".to_string(),
397                ]),
398                severity: 90.0,
399                active: true,
400            },
401            FraudSignature {
402                id: 3,
403                name: "Suspicious Time Window".to_string(),
404                pattern: SignaturePattern::TimeWindow {
405                    events: vec![
406                        "login".to_string(),
407                        "high_value_purchase".to_string(),
408                        "logout".to_string(),
409                    ],
410                    window_secs: 300,
411                },
412                severity: 80.0,
413                active: true,
414            },
415            FraudSignature {
416                id: 4,
417                name: "Gift Card Fraud Pattern".to_string(),
418                pattern: SignaturePattern::Regex("gift.*card".to_string()),
419                severity: 60.0,
420                active: true,
421            },
422        ]
423    }
424}
425
426impl GpuKernel for FraudSignatureDetection {
427    fn metadata(&self) -> &KernelMetadata {
428        &self.metadata
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    fn create_login_attack_events() -> Vec<UserEvent> {
437        let base_ts = 1700000000u64;
438        (0..10)
439            .map(|i| UserEvent {
440                id: i as u64,
441                user_id: 100,
442                event_type: "login_attempt".to_string(),
443                timestamp: base_ts + (i as u64 * 5), // 5 seconds apart
444                attributes: HashMap::new(),
445                session_id: None,
446                device_id: Some("unknown".to_string()),
447                ip_address: Some("10.0.0.1".to_string()),
448                location: Some("XX".to_string()),
449            })
450            .collect()
451    }
452
453    fn create_account_takeover_events() -> Vec<UserEvent> {
454        let base_ts = 1700000000u64;
455        vec![
456            UserEvent {
457                id: 1,
458                user_id: 100,
459                event_type: "password_reset".to_string(),
460                timestamp: base_ts,
461                attributes: HashMap::new(),
462                session_id: None,
463                device_id: Some("new_device".to_string()),
464                ip_address: None,
465                location: None,
466            },
467            UserEvent {
468                id: 2,
469                user_id: 100,
470                event_type: "login".to_string(),
471                timestamp: base_ts + 60,
472                attributes: HashMap::new(),
473                session_id: Some(1),
474                device_id: Some("new_device".to_string()),
475                ip_address: None,
476                location: None,
477            },
478            UserEvent {
479                id: 3,
480                user_id: 100,
481                event_type: "profile_change".to_string(),
482                timestamp: base_ts + 120,
483                attributes: HashMap::new(),
484                session_id: Some(1),
485                device_id: Some("new_device".to_string()),
486                ip_address: None,
487                location: None,
488            },
489        ]
490    }
491
492    #[test]
493    fn test_fraud_signature_metadata() {
494        let kernel = FraudSignatureDetection::new();
495        assert_eq!(kernel.metadata().id, "behavioral/fraud-signatures");
496        assert_eq!(kernel.metadata().domain, Domain::BehavioralAnalytics);
497    }
498
499    #[test]
500    fn test_count_threshold_detection() {
501        let events = create_login_attack_events();
502        let signatures = vec![FraudSignature {
503            id: 1,
504            name: "Rapid Login Attempts".to_string(),
505            pattern: SignaturePattern::CountThreshold {
506                event_type: "login_attempt".to_string(),
507                count: 5,
508                window_secs: 60,
509            },
510            severity: 70.0,
511            active: true,
512        }];
513
514        let matches = FraudSignatureDetection::compute(&events, &signatures);
515
516        assert!(!matches.is_empty(), "Should detect rapid login pattern");
517        assert_eq!(matches[0].signature_id, 1);
518        assert!(matches[0].score > 50.0);
519    }
520
521    #[test]
522    fn test_sequence_detection() {
523        let events = create_account_takeover_events();
524        let signatures = vec![FraudSignature {
525            id: 2,
526            name: "Account Takeover Sequence".to_string(),
527            pattern: SignaturePattern::EventSequence(vec![
528                "password_reset".to_string(),
529                "login".to_string(),
530                "profile_change".to_string(),
531            ]),
532            severity: 90.0,
533            active: true,
534        }];
535
536        let matches = FraudSignatureDetection::compute(&events, &signatures);
537
538        assert!(
539            !matches.is_empty(),
540            "Should detect account takeover sequence"
541        );
542        assert_eq!(matches[0].signature_id, 2);
543        assert_eq!(matches[0].matched_events.len(), 3);
544    }
545
546    #[test]
547    fn test_time_window_detection() {
548        let base_ts = 1700000000u64;
549        let events = vec![
550            UserEvent {
551                id: 1,
552                user_id: 100,
553                event_type: "login".to_string(),
554                timestamp: base_ts,
555                attributes: HashMap::new(),
556                session_id: Some(1),
557                device_id: None,
558                ip_address: None,
559                location: None,
560            },
561            UserEvent {
562                id: 2,
563                user_id: 100,
564                event_type: "high_value_purchase".to_string(),
565                timestamp: base_ts + 120,
566                attributes: HashMap::new(),
567                session_id: Some(1),
568                device_id: None,
569                ip_address: None,
570                location: None,
571            },
572            UserEvent {
573                id: 3,
574                user_id: 100,
575                event_type: "logout".to_string(),
576                timestamp: base_ts + 180,
577                attributes: HashMap::new(),
578                session_id: Some(1),
579                device_id: None,
580                ip_address: None,
581                location: None,
582            },
583        ];
584
585        let signatures = vec![FraudSignature {
586            id: 3,
587            name: "Suspicious Time Window".to_string(),
588            pattern: SignaturePattern::TimeWindow {
589                events: vec![
590                    "login".to_string(),
591                    "high_value_purchase".to_string(),
592                    "logout".to_string(),
593                ],
594                window_secs: 300,
595            },
596            severity: 80.0,
597            active: true,
598        }];
599
600        let matches = FraudSignatureDetection::compute(&events, &signatures);
601
602        assert!(!matches.is_empty(), "Should detect time window pattern");
603        assert_eq!(matches[0].signature_id, 3);
604    }
605
606    #[test]
607    fn test_regex_detection() {
608        let mut attrs = HashMap::new();
609        attrs.insert(
610            "item_type".to_string(),
611            EventValue::String("gift_card_100".to_string()),
612        );
613
614        let events = vec![UserEvent {
615            id: 1,
616            user_id: 100,
617            event_type: "purchase".to_string(),
618            timestamp: 1700000000,
619            attributes: attrs,
620            session_id: Some(1),
621            device_id: None,
622            ip_address: None,
623            location: None,
624        }];
625
626        let signatures = vec![FraudSignature {
627            id: 4,
628            name: "Gift Card Fraud Pattern".to_string(),
629            pattern: SignaturePattern::Regex("gift_card".to_string()),
630            severity: 60.0,
631            active: true,
632        }];
633
634        let matches = FraudSignatureDetection::compute(&events, &signatures);
635
636        assert!(!matches.is_empty(), "Should detect gift card pattern");
637        assert_eq!(matches[0].signature_id, 4);
638    }
639
640    #[test]
641    fn test_inactive_signature_ignored() {
642        let events = create_login_attack_events();
643        let signatures = vec![FraudSignature {
644            id: 1,
645            name: "Rapid Login Attempts".to_string(),
646            pattern: SignaturePattern::CountThreshold {
647                event_type: "login_attempt".to_string(),
648                count: 5,
649                window_secs: 60,
650            },
651            severity: 70.0,
652            active: false, // Inactive
653        }];
654
655        let matches = FraudSignatureDetection::compute(&events, &signatures);
656
657        assert!(matches.is_empty(), "Inactive signatures should be ignored");
658    }
659
660    #[test]
661    fn test_event_attributes_match() {
662        let mut attrs = HashMap::new();
663        attrs.insert("country".to_string(), EventValue::String("XX".to_string()));
664        attrs.insert("amount".to_string(), EventValue::Number(10000.0));
665
666        let events = vec![UserEvent {
667            id: 1,
668            user_id: 100,
669            event_type: "transfer".to_string(),
670            timestamp: 1700000000,
671            attributes: attrs,
672            session_id: None,
673            device_id: None,
674            ip_address: None,
675            location: None,
676        }];
677
678        let mut required_attrs = HashMap::new();
679        required_attrs.insert("country".to_string(), EventValue::String("XX".to_string()));
680
681        let signatures = vec![FraudSignature {
682            id: 5,
683            name: "High Risk Country Transfer".to_string(),
684            pattern: SignaturePattern::EventAttributes("transfer".to_string(), required_attrs),
685            severity: 75.0,
686            active: true,
687        }];
688
689        let matches = FraudSignatureDetection::compute(&events, &signatures);
690
691        assert!(!matches.is_empty(), "Should match event attributes");
692        assert_eq!(matches[0].signature_id, 5);
693    }
694
695    #[test]
696    fn test_standard_signatures() {
697        let signatures = FraudSignatureDetection::standard_signatures();
698
699        assert!(!signatures.is_empty());
700        assert!(signatures.iter().all(|s| s.active));
701    }
702
703    #[test]
704    fn test_no_match() {
705        let events = vec![UserEvent {
706            id: 1,
707            user_id: 100,
708            event_type: "normal_activity".to_string(),
709            timestamp: 1700000000,
710            attributes: HashMap::new(),
711            session_id: Some(1),
712            device_id: None,
713            ip_address: None,
714            location: None,
715        }];
716
717        let signatures = FraudSignatureDetection::standard_signatures();
718        let matches = FraudSignatureDetection::compute(&events, &signatures);
719
720        assert!(matches.is_empty(), "Should not match normal activity");
721    }
722}