Skip to main content

datasynth_banking/typologies/
fraud.rs

1//! Fraud typology implementations (account takeover, fake vendors, BEC).
2
3use chrono::{DateTime, Datelike, NaiveDate, Utc};
4use datasynth_core::models::banking::{
5    AmlTypology, Direction, LaunderingStage, Sophistication, TransactionCategory,
6    TransactionChannel,
7};
8use datasynth_core::DeterministicUuidFactory;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12
13use crate::models::{BankAccount, BankTransaction, BankingCustomer, CounterpartyRef};
14
15/// Fraud pattern injector.
16///
17/// Covers multiple fraud typologies:
18/// - Account takeover (unauthorized access)
19/// - Fake vendor schemes
20/// - Business email compromise (BEC)
21/// - Authorized push payment (APP) fraud
22pub struct FraudInjector {
23    rng: ChaCha8Rng,
24    uuid_factory: DeterministicUuidFactory,
25}
26
27impl FraudInjector {
28    /// Create a new fraud injector.
29    pub fn new(seed: u64) -> Self {
30        Self {
31            rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(7200)),
32            uuid_factory: DeterministicUuidFactory::new(
33                seed,
34                datasynth_core::GeneratorType::Anomaly,
35            ),
36        }
37    }
38
39    /// Generate account takeover fraud transactions.
40    ///
41    /// Pattern: Unauthorized access followed by rapid fund extraction
42    pub fn generate_account_takeover(
43        &mut self,
44        _customer: &BankingCustomer,
45        account: &BankAccount,
46        start_date: NaiveDate,
47        _end_date: NaiveDate,
48        sophistication: Sophistication,
49    ) -> Vec<BankTransaction> {
50        let mut transactions = Vec::new();
51
52        // ATO parameters based on sophistication
53        let (num_extractions, max_amount, time_window_hours) = match sophistication {
54            Sophistication::Basic => (1..3, 5_000.0, 1..4),
55            Sophistication::Standard => (2..4, 15_000.0, 1..8),
56            Sophistication::Professional => (3..6, 50_000.0, 2..12),
57            Sophistication::Advanced => (4..8, 100_000.0, 4..24),
58            Sophistication::StateLevel => (5..10, 250_000.0, 8..48),
59        };
60
61        let extractions = self.rng.gen_range(num_extractions);
62        let scenario_id = format!("ATO-{:06}", self.rng.gen::<u32>());
63
64        // Account takeover typically happens in a short window
65        let takeover_date = start_date;
66        let mut current_hour = self.rng.gen_range(0..12);
67
68        for i in 0..extractions {
69            // Time progresses within the window
70            let hour_offset = self.rng.gen_range(time_window_hours.clone());
71            current_hour = (current_hour + hour_offset) % 24;
72
73            let timestamp = takeover_date
74                .and_hms_opt(
75                    current_hour as u32,
76                    self.rng.gen_range(0..60),
77                    self.rng.gen_range(0..60),
78                )
79                .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
80                .unwrap_or_else(Utc::now);
81
82            // Each extraction varies in amount
83            let amount = self.rng.gen_range(500.0..max_amount);
84
85            // Varying channels for extraction
86            let (channel, category, counterparty, reference) = self.random_ato_extraction(i);
87
88            let txn = BankTransaction::new(
89                self.uuid_factory.next(),
90                account.account_id,
91                Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
92                &account.currency,
93                Direction::Outbound,
94                channel,
95                category,
96                counterparty,
97                &reference,
98                timestamp,
99            )
100            .mark_suspicious(AmlTypology::AccountTakeover, &scenario_id)
101            .with_laundering_stage(LaunderingStage::NotApplicable)
102            .with_scenario(&scenario_id, i as u32);
103
104            transactions.push(txn);
105        }
106
107        // For sophisticated cases, add reconnaissance-like activity
108        if matches!(
109            sophistication,
110            Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
111        ) {
112            // Small test transactions before main extraction
113            let test_timestamp = takeover_date
114                .and_hms_opt(self.rng.gen_range(6..12), self.rng.gen_range(0..60), 0)
115                .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
116                .unwrap_or_else(Utc::now);
117
118            let test_txn = BankTransaction::new(
119                self.uuid_factory.next(),
120                account.account_id,
121                Decimal::from_f64_retain(self.rng.gen_range(1.0..10.0)).unwrap_or(Decimal::ONE),
122                &account.currency,
123                Direction::Outbound,
124                TransactionChannel::CardNotPresent,
125                TransactionCategory::Shopping,
126                CounterpartyRef::merchant_by_name("Test Merchant", "5999"),
127                "Small test purchase",
128                test_timestamp,
129            )
130            .mark_suspicious(AmlTypology::AccountTakeover, &scenario_id)
131            .with_laundering_stage(LaunderingStage::NotApplicable)
132            .with_scenario(&scenario_id, extractions as u32);
133
134            transactions.insert(0, test_txn);
135        }
136
137        transactions
138    }
139
140    /// Generate fake vendor fraud transactions.
141    ///
142    /// Pattern: Fictitious vendor receiving payments for non-existent goods/services
143    pub fn generate_fake_vendor(
144        &mut self,
145        _customer: &BankingCustomer,
146        account: &BankAccount,
147        start_date: NaiveDate,
148        end_date: NaiveDate,
149        sophistication: Sophistication,
150    ) -> Vec<BankTransaction> {
151        let mut transactions = Vec::new();
152
153        // Fake vendor parameters based on sophistication
154        let (num_payments, payment_range, interval_days) = match sophistication {
155            Sophistication::Basic => (2..4, 1_000.0..10_000.0, 7..14),
156            Sophistication::Standard => (3..6, 5_000.0..30_000.0, 14..30),
157            Sophistication::Professional => (4..8, 10_000.0..75_000.0, 21..45),
158            Sophistication::Advanced => (6..12, 25_000.0..150_000.0, 30..60),
159            Sophistication::StateLevel => (8..20, 50_000.0..500_000.0, 45..90),
160        };
161
162        let payments = self.rng.gen_range(num_payments);
163        let scenario_id = format!("FKV-{:06}", self.rng.gen::<u32>());
164
165        // Create the fake vendor
166        let fake_vendor = self.random_fake_vendor();
167        let available_days = (end_date - start_date).num_days().max(1);
168        let mut current_date = start_date;
169
170        for i in 0..payments {
171            let timestamp = self.random_timestamp(current_date);
172            let amount = self.rng.gen_range(payment_range.clone());
173
174            // Create invoice-like reference
175            let invoice_ref = format!(
176                "INV-{:04}-{:06}",
177                current_date.year() % 100,
178                self.rng.gen::<u32>() % 1_000_000
179            );
180
181            let txn = BankTransaction::new(
182                self.uuid_factory.next(),
183                account.account_id,
184                Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
185                &account.currency,
186                Direction::Outbound,
187                TransactionChannel::Ach,
188                TransactionCategory::Other,
189                CounterpartyRef::business(&fake_vendor.0),
190                &format!("{} - {}", fake_vendor.1, invoice_ref),
191                timestamp,
192            )
193            .mark_suspicious(AmlTypology::FakeVendor, &scenario_id)
194            .with_laundering_stage(LaunderingStage::Placement)
195            .with_scenario(&scenario_id, i as u32);
196
197            transactions.push(txn);
198
199            // Move to next payment date
200            let interval = self.rng.gen_range(interval_days.clone()) as i64;
201            current_date += chrono::Duration::days(interval);
202
203            if current_date > end_date || (current_date - start_date).num_days() > available_days {
204                break;
205            }
206        }
207
208        // Apply spoofing for sophisticated patterns
209        if matches!(
210            sophistication,
211            Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
212        ) {
213            for txn in &mut transactions {
214                txn.is_spoofed = true;
215                txn.spoofing_intensity = Some(sophistication.spoofing_intensity());
216            }
217        }
218
219        transactions
220    }
221
222    /// Generate business email compromise (BEC) fraud transactions.
223    ///
224    /// Pattern: Large urgent payment to "new" bank details, often international
225    pub fn generate_bec(
226        &mut self,
227        _customer: &BankingCustomer,
228        account: &BankAccount,
229        start_date: NaiveDate,
230        _end_date: NaiveDate,
231        sophistication: Sophistication,
232    ) -> Vec<BankTransaction> {
233        let mut transactions = Vec::new();
234
235        // BEC typically involves 1-3 large payments
236        let (num_payments, amount_range) = match sophistication {
237            Sophistication::Basic => (1..2, 25_000.0..75_000.0),
238            Sophistication::Standard => (1..2, 50_000.0..150_000.0),
239            Sophistication::Professional => (1..3, 100_000.0..500_000.0),
240            Sophistication::Advanced => (2..3, 250_000.0..1_000_000.0),
241            Sophistication::StateLevel => (2..4, 500_000.0..5_000_000.0),
242        };
243
244        let payments = self.rng.gen_range(num_payments);
245        let scenario_id = format!("BEC-{:06}", self.rng.gen::<u32>());
246
247        // BEC typically happens quickly after the initial compromise
248        let mut current_date = start_date;
249
250        for i in 0..payments {
251            let timestamp = self.random_timestamp(current_date);
252            let amount = self.rng.gen_range(amount_range.clone());
253
254            let (recipient, reference) = self.random_bec_recipient();
255
256            let txn = BankTransaction::new(
257                self.uuid_factory.next(),
258                account.account_id,
259                Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
260                &account.currency,
261                Direction::Outbound,
262                TransactionChannel::Swift, // International wire
263                TransactionCategory::Other,
264                CounterpartyRef::business(&recipient),
265                &reference,
266                timestamp,
267            )
268            .mark_suspicious(AmlTypology::BusinessEmailCompromise, &scenario_id)
269            .with_laundering_stage(LaunderingStage::Placement)
270            .with_scenario(&scenario_id, i as u32);
271
272            transactions.push(txn);
273
274            // Short interval between BEC payments
275            current_date += chrono::Duration::days(self.rng.gen_range(1..3));
276        }
277
278        transactions
279    }
280
281    /// Generate authorized push payment (APP) fraud transactions.
282    ///
283    /// Pattern: Victim manipulated into authorizing payments to fraudster
284    pub fn generate_app_fraud(
285        &mut self,
286        _customer: &BankingCustomer,
287        account: &BankAccount,
288        start_date: NaiveDate,
289        end_date: NaiveDate,
290        sophistication: Sophistication,
291    ) -> Vec<BankTransaction> {
292        let mut transactions = Vec::new();
293
294        // APP fraud parameters
295        let (num_payments, amount_range, urgency_factor) = match sophistication {
296            Sophistication::Basic => (1..2, 500.0..5_000.0, 0.8),
297            Sophistication::Standard => (2..4, 1_000.0..15_000.0, 0.7),
298            Sophistication::Professional => (3..6, 5_000.0..50_000.0, 0.6),
299            Sophistication::Advanced => (4..8, 10_000.0..100_000.0, 0.5),
300            Sophistication::StateLevel => (5..10, 25_000.0..250_000.0, 0.4),
301        };
302
303        let payments = self.rng.gen_range(num_payments);
304        let scenario_id = format!("APP-{:06}", self.rng.gen::<u32>());
305
306        let scam_type = self.random_app_scam_type();
307        let available_days = (end_date - start_date).num_days().max(1);
308        let mut current_date = start_date;
309
310        for i in 0..payments {
311            let timestamp = self.random_timestamp(current_date);
312            let amount = self.rng.gen_range(amount_range.clone());
313
314            let txn = BankTransaction::new(
315                self.uuid_factory.next(),
316                account.account_id,
317                Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
318                &account.currency,
319                Direction::Outbound,
320                TransactionChannel::RealTimePayment,
321                TransactionCategory::TransferOut,
322                CounterpartyRef::person(&scam_type.0),
323                &scam_type.1,
324                timestamp,
325            )
326            .mark_suspicious(AmlTypology::AuthorizedPushPayment, &scenario_id)
327            .with_laundering_stage(LaunderingStage::NotApplicable)
328            .with_scenario(&scenario_id, i as u32);
329
330            transactions.push(txn);
331
332            // Interval between payments (urgency = shorter intervals)
333            let base_interval = self.rng.gen_range(1..7) as f64;
334            let interval = (base_interval * urgency_factor).max(1.0) as i64;
335            current_date += chrono::Duration::days(interval);
336
337            if current_date > end_date || (current_date - start_date).num_days() > available_days {
338                break;
339            }
340        }
341
342        transactions
343    }
344
345    /// Generate random ATO extraction method.
346    fn random_ato_extraction(
347        &mut self,
348        index: usize,
349    ) -> (
350        TransactionChannel,
351        TransactionCategory,
352        CounterpartyRef,
353        String,
354    ) {
355        let extractions = [
356            (
357                TransactionChannel::Wire,
358                TransactionCategory::TransferOut,
359                CounterpartyRef::person("External Account"),
360                "External transfer".to_string(),
361            ),
362            (
363                TransactionChannel::Ach,
364                TransactionCategory::TransferOut,
365                CounterpartyRef::person("Linked Account"),
366                "ACH transfer out".to_string(),
367            ),
368            (
369                TransactionChannel::CardNotPresent,
370                TransactionCategory::Shopping,
371                CounterpartyRef::merchant_by_name("Online Store", "5999"),
372                "Online purchase".to_string(),
373            ),
374            (
375                TransactionChannel::Atm,
376                TransactionCategory::AtmWithdrawal,
377                CounterpartyRef::atm("ATM"),
378                "ATM withdrawal".to_string(),
379            ),
380            (
381                TransactionChannel::CardNotPresent,
382                TransactionCategory::Shopping,
383                CounterpartyRef::merchant_by_name("Gift Card Vendor", "5815"),
384                "Gift card purchase".to_string(),
385            ),
386        ];
387
388        let idx = (index + self.rng.gen_range(0..extractions.len())) % extractions.len();
389        extractions[idx].clone()
390    }
391
392    /// Generate random fake vendor details.
393    fn random_fake_vendor(&mut self) -> (String, String) {
394        let vendors = [
395            ("ABC Consulting Services LLC", "Consulting services"),
396            ("Generic Supplies Inc", "Office supplies"),
397            ("Tech Solutions Partners", "IT services"),
398            ("Professional Services Group", "Professional fees"),
399            ("Strategic Advisory LLC", "Advisory services"),
400            ("Business Support Services", "Business support"),
401            ("Enterprise Solutions Corp", "Enterprise solutions"),
402            ("Market Research Associates", "Research services"),
403            ("Quality Assurance Partners", "QA services"),
404            ("Operational Excellence LLC", "Operations consulting"),
405        ];
406
407        let idx = self.rng.gen_range(0..vendors.len());
408        (vendors[idx].0.to_string(), vendors[idx].1.to_string())
409    }
410
411    /// Generate random BEC recipient details.
412    fn random_bec_recipient(&mut self) -> (String, String) {
413        let recipients = [
414            (
415                "International Trade Co Ltd",
416                "URGENT: Updated payment details - Invoice payment",
417            ),
418            (
419                "Overseas Partner Holdings",
420                "Wire transfer - NEW BANK DETAILS",
421            ),
422            (
423                "Foreign Supplier Pte Ltd",
424                "Payment for goods - UPDATED ACCOUNT",
425            ),
426            (
427                "Global Trading Services",
428                "URGENT: Supplier payment - new instructions",
429            ),
430            (
431                "Asian Manufacturing Ltd",
432                "Invoice settlement - REVISED BANK",
433            ),
434        ];
435
436        let idx = self.rng.gen_range(0..recipients.len());
437        (recipients[idx].0.to_string(), recipients[idx].1.to_string())
438    }
439
440    /// Generate random APP scam type.
441    fn random_app_scam_type(&mut self) -> (String, String) {
442        let scam_types = [
443            ("HMRC Tax Department", "Tax refund processing fee"),
444            ("Investment Advisor", "Investment opportunity"),
445            ("Tech Support Services", "Computer repair services"),
446            ("Romantic Partner", "Emergency funds needed"),
447            ("Police Officer", "Safe account transfer"),
448            ("Bank Security", "Account protection transfer"),
449            ("Lottery Commission", "Prize claim fee"),
450            ("Crypto Investment", "Cryptocurrency investment"),
451        ];
452
453        let idx = self.rng.gen_range(0..scam_types.len());
454        (scam_types[idx].0.to_string(), scam_types[idx].1.to_string())
455    }
456
457    /// Generate random timestamp for a date.
458    fn random_timestamp(&mut self, date: NaiveDate) -> DateTime<Utc> {
459        let hour: u32 = self.rng.gen_range(6..23);
460        let minute: u32 = self.rng.gen_range(0..60);
461        let second: u32 = self.rng.gen_range(0..60);
462
463        date.and_hms_opt(hour, minute, second)
464            .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
465            .unwrap_or_else(Utc::now)
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use uuid::Uuid;
473
474    fn create_test_customer() -> BankingCustomer {
475        BankingCustomer::new_retail(
476            Uuid::new_v4(),
477            "Test",
478            "User",
479            "US",
480            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
481        )
482    }
483
484    fn create_test_account(customer: &BankingCustomer) -> BankAccount {
485        BankAccount::new(
486            Uuid::new_v4(),
487            "****1234".to_string(),
488            datasynth_core::models::banking::BankAccountType::Checking,
489            customer.customer_id,
490            "USD",
491            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
492        )
493    }
494
495    #[test]
496    fn test_account_takeover_generation() {
497        let mut injector = FraudInjector::new(12345);
498        let customer = create_test_customer();
499        let account = create_test_account(&customer);
500
501        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
502        let end = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap();
503
504        let transactions = injector.generate_account_takeover(
505            &customer,
506            &account,
507            start,
508            end,
509            Sophistication::Standard,
510        );
511
512        assert!(!transactions.is_empty());
513
514        // All should be outbound (extraction)
515        for txn in &transactions {
516            assert!(txn.is_suspicious);
517            assert_eq!(txn.suspicion_reason, Some(AmlTypology::AccountTakeover));
518            assert_eq!(txn.direction, Direction::Outbound);
519        }
520    }
521
522    #[test]
523    fn test_fake_vendor_generation() {
524        let mut injector = FraudInjector::new(54321);
525        let customer = create_test_customer();
526        let account = create_test_account(&customer);
527
528        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
529        let end = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
530
531        let transactions = injector.generate_fake_vendor(
532            &customer,
533            &account,
534            start,
535            end,
536            Sophistication::Professional,
537        );
538
539        assert!(!transactions.is_empty());
540        assert!(transactions.len() >= 4); // Professional has 4-8 payments
541
542        for txn in &transactions {
543            assert!(txn.is_suspicious);
544            assert_eq!(txn.suspicion_reason, Some(AmlTypology::FakeVendor));
545        }
546    }
547
548    #[test]
549    fn test_bec_generation() {
550        let mut injector = FraudInjector::new(11111);
551        let customer = create_test_customer();
552        let account = create_test_account(&customer);
553
554        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
555        let end = NaiveDate::from_ymd_opt(2024, 1, 14).unwrap();
556
557        let transactions =
558            injector.generate_bec(&customer, &account, start, end, Sophistication::Advanced);
559
560        assert!(!transactions.is_empty());
561
562        for txn in &transactions {
563            assert!(txn.is_suspicious);
564            assert_eq!(
565                txn.suspicion_reason,
566                Some(AmlTypology::BusinessEmailCompromise)
567            );
568            // BEC typically uses SWIFT for international wires
569            assert_eq!(txn.channel, TransactionChannel::Swift);
570        }
571    }
572
573    #[test]
574    fn test_app_fraud_generation() {
575        let mut injector = FraudInjector::new(99999);
576        let customer = create_test_customer();
577        let account = create_test_account(&customer);
578
579        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
580        let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
581
582        let transactions =
583            injector.generate_app_fraud(&customer, &account, start, end, Sophistication::Standard);
584
585        assert!(!transactions.is_empty());
586
587        for txn in &transactions {
588            assert!(txn.is_suspicious);
589            assert_eq!(
590                txn.suspicion_reason,
591                Some(AmlTypology::AuthorizedPushPayment)
592            );
593        }
594    }
595}