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    ]
27});
28
29fn luhn_valid(pan: &str) -> bool {
30    let digits: Vec<u8> = pan
31        .as_bytes()
32        .iter()
33        .filter_map(|b| {
34            if b.is_ascii_digit() {
35                Some(b - b'0')
36            } else {
37                None
38            }
39        })
40        .collect();
41    if digits.len() < 13 {
42        return false;
43    }
44    let sum: u32 = digits
45        .iter()
46        .rev()
47        .enumerate()
48        .map(|(i, &d)| {
49            if i % 2 == 1 {
50                let doubled = d as u32 * 2;
51                if doubled > 9 { doubled - 9 } else { doubled }
52            } else {
53                d as u32
54            }
55        })
56        .sum();
57    sum.is_multiple_of(10)
58}
59
60pub struct DataLeakDetector;
61
62impl Detector for DataLeakDetector {
63    fn name(&self) -> &'static str {
64        "data_leak"
65    }
66
67    fn detect(&self, input: &str) -> Option<DetectionResult> {
68        if let Some(m) = CC_PAN.find(input)
69            && luhn_valid(m.as_str())
70        {
71            return Some(DetectionResult {
72                attack_type: "data_leak".into(),
73                category: AttackCategory::File,
74                severity: Severity::Critical,
75                matched_pattern: m.as_str().to_string(),
76                offset: m.start(),
77                message: "Sensitive data leak detected (credit card)".into(),
78            });
79        }
80        for re in PATTERNS.iter() {
81            if let Some(m) = re.find(input) {
82                return Some(DetectionResult {
83                    attack_type: "data_leak".into(),
84                    category: AttackCategory::File,
85                    severity: Severity::Critical,
86                    matched_pattern: m.as_str().to_string(),
87                    offset: m.start(),
88                    message: "Sensitive data leak detected".into(),
89                });
90            }
91        }
92        None
93    }
94}