Skip to main content

security_rust/
lib.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use regex::Regex;
4
5pub mod data;
6pub mod file;
7pub mod injection;
8pub mod protocol;
9pub mod result;
10pub mod scanner;
11
12pub use result::{AttackCategory, DetectionResult, Severity};
13pub use scanner::{Scanner, ScannerBuilder};
14
15pub trait Detector: Send + Sync {
16    fn name(&self) -> &'static str;
17    fn detect(&self, input: &str) -> Option<DetectionResult>;
18}
19
20pub(crate) fn regex_detect(
21    patterns: &[Regex],
22    name: &'static str,
23    category: AttackCategory,
24    severity: Severity,
25    message: &'static str,
26    input: &str,
27) -> Option<DetectionResult> {
28    for re in patterns {
29        if let Some(m) = re.find(input) {
30            return Some(DetectionResult {
31                attack_type: name.to_string(),
32                category,
33                severity,
34                matched_pattern: m.as_str().to_string(),
35                offset: m.start(),
36                message: message.into(),
37            });
38        }
39    }
40    None
41}
42
43#[cfg(test)]
44pub(crate) mod test_helpers {
45    use super::*;
46
47    pub(crate) fn assert_detected<D: Detector>(
48        d: &D,
49        input: &str,
50        category: AttackCategory,
51        severity: Severity,
52    ) {
53        let r = d.detect(input).expect("expected detection");
54        assert_eq!(r.attack_type, d.name());
55        assert_eq!(r.category, category);
56        assert_eq!(r.severity, severity);
57        assert!(!r.matched_pattern.is_empty(), "matched_pattern empty");
58        assert!(
59            r.offset <= input.len(),
60            "offset {} > len {}",
61            r.offset,
62            input.len()
63        );
64        assert_eq!(
65            &input[r.offset..r.offset + r.matched_pattern.len()],
66            r.matched_pattern
67        );
68        assert!(!r.message.is_empty());
69    }
70
71    pub(crate) fn assert_clean<D: Detector>(d: &D, input: &str) {
72        assert!(d.detect(input).is_none(), "not detected: {input:?}");
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn detector_trait_object_is_send_sync() {
82        let detector: Box<dyn Detector> = Box::new(injection::XssDetector);
83        assert_eq!(detector.name(), "xss");
84    }
85
86    #[test]
87    fn detector_name_is_static_str() {
88        let name: &'static str = injection::XssDetector.name();
89        assert_eq!(name, "xss");
90    }
91}