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;
11pub mod score;
12pub mod session;
13pub mod throttle;
14
15pub use result::{AttackCategory, DetectionResult, Severity};
16pub use scanner::{Scanner, ScannerBuilder};
17pub use score::{RiskAssessment, RiskLevel, assess};
18pub use session::{
19    Decision, LoginPoint, MemoryStore, RequestContext, SessionConfig, SessionError, SessionGuard,
20    SessionRecord, SessionStore, SessionThreat, SessionVerdict, StoreError,
21};
22pub use throttle::{
23    MemoryThrottleStore, Throttle, ThrottleConfig, ThrottleDecision, ThrottleOutcome, ThrottleStore,
24};
25
26pub trait Detector: Send + Sync {
27    fn name(&self) -> &'static str;
28    fn detect(&self, input: &str) -> Option<DetectionResult>;
29}
30
31pub(crate) fn regex_detect(
32    patterns: &[Regex],
33    name: &'static str,
34    category: AttackCategory,
35    severity: Severity,
36    message: &'static str,
37    input: &str,
38) -> Option<DetectionResult> {
39    for re in patterns {
40        if let Some(m) = re.find(input) {
41            return Some(DetectionResult {
42                attack_type: name.to_string(),
43                category,
44                severity,
45                matched_pattern: m.as_str().to_string(),
46                offset: m.start(),
47                message: message.into(),
48            });
49        }
50    }
51    None
52}
53
54#[cfg(test)]
55pub(crate) mod test_helpers {
56    use super::*;
57
58    pub(crate) fn assert_detected<D: Detector>(
59        d: &D,
60        input: &str,
61        category: AttackCategory,
62        severity: Severity,
63    ) {
64        let r = d.detect(input).expect("expected detection");
65        assert_eq!(r.attack_type, d.name());
66        assert_eq!(r.category, category);
67        assert_eq!(r.severity, severity);
68        assert!(!r.matched_pattern.is_empty(), "matched_pattern empty");
69        assert!(
70            r.offset <= input.len(),
71            "offset {} > len {}",
72            r.offset,
73            input.len()
74        );
75        assert_eq!(
76            &input[r.offset..r.offset + r.matched_pattern.len()],
77            r.matched_pattern
78        );
79        assert!(!r.message.is_empty());
80    }
81
82    pub(crate) fn assert_clean<D: Detector>(d: &D, input: &str) {
83        assert!(d.detect(input).is_none(), "not detected: {input:?}");
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn detector_trait_object_is_send_sync() {
93        let detector: Box<dyn Detector> = Box::new(injection::XssDetector);
94        assert_eq!(detector.name(), "xss");
95    }
96
97    #[test]
98    fn detector_name_is_static_str() {
99        let name: &'static str = injection::XssDetector.name();
100        assert_eq!(name, "xss");
101    }
102}