Skip to main content

security_rust/file/
data_leak.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity};
7
8static CC_PAN: LazyLock<Regex> = LazyLock::new(|| {
9    Regex::new(r"\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b").unwrap()
10});
11
12static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
13    vec![
14        Regex::new(r"AKIA[0-9A-Z]{16}").unwrap(),
15        Regex::new(r"-----BEGIN\s*(?:RSA\s*)?PRIVATE\s*KEY").unwrap(),
16        Regex::new(r"-----BEGIN\s*CERTIFICATE").unwrap(),
17        Regex::new(r"-----BEGIN\s*DSA\s*PRIVATE").unwrap(),
18        Regex::new(r"-----BEGIN\s*EC\s*PRIVATE").unwrap(),
19        Regex::new(r"-----BEGIN\s*PGP\s*PRIVATE").unwrap(),
20        Regex::new(r"sk-[A-Za-z0-9]{32,}").unwrap(),
21        Regex::new(r"(?i)mongodb(?:\+srv)?://[^/\s]+").unwrap(),
22        Regex::new(r"(?i)mysql://[^/\s]+").unwrap(),
23        Regex::new(r"(?i)postgres(?:ql)?://[^/\s]+").unwrap(),
24        Regex::new(r"(?i)redis://[^/\s]+").unwrap(),
25        Regex::new(r"(?i)jdbc:[a-z]+://").unwrap(),
26        Regex::new(r"(?i)eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+").unwrap(),
27    ]
28});
29
30fn luhn_valid(pan: &str) -> bool {
31    let digits: Vec<u8> = pan
32        .as_bytes()
33        .iter()
34        .filter_map(|b| {
35            if b.is_ascii_digit() {
36                Some(b - b'0')
37            } else {
38                None
39            }
40        })
41        .collect();
42    if digits.len() < 13 {
43        return false;
44    }
45    let sum: u32 = digits
46        .iter()
47        .rev()
48        .enumerate()
49        .map(|(i, &d)| {
50            if i % 2 == 1 {
51                let doubled = d as u32 * 2;
52                if doubled > 9 { doubled - 9 } else { doubled }
53            } else {
54                d as u32
55            }
56        })
57        .sum();
58    sum.is_multiple_of(10)
59}
60
61pub struct DataLeakDetector;
62
63impl Detector for DataLeakDetector {
64    fn name(&self) -> &'static str {
65        "data_leak"
66    }
67
68    fn detect(&self, input: &str) -> Option<DetectionResult> {
69        if let Some(m) = CC_PAN.find(input)
70            && luhn_valid(m.as_str())
71        {
72            return Some(DetectionResult {
73                attack_type: "data_leak".into(),
74                category: AttackCategory::File,
75                severity: Severity::Critical,
76                matched_pattern: m.as_str().to_string(),
77                offset: m.start(),
78                message: "Sensitive data leak detected (credit card)".into(),
79            });
80        }
81        for re in PATTERNS.iter() {
82            if let Some(m) = re.find(input) {
83                return Some(DetectionResult {
84                    attack_type: "data_leak".into(),
85                    category: AttackCategory::File,
86                    severity: Severity::Critical,
87                    matched_pattern: m.as_str().to_string(),
88                    offset: m.start(),
89                    message: "Sensitive data leak detected".into(),
90                });
91            }
92        }
93        None
94    }
95}