Skip to main content

security_rust/
scanner.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use crate::data::{
4    CsvInjectionDetector, DeserializationDetector, JwtAttackDetector, MailHeaderDetector,
5    PrototypePollutionDetector,
6};
7use crate::file::{DataLeakDetector, PathTraversalDetector, UploadDetector};
8use crate::injection::{
9    CommandInjectionDetector, GraphQlInjectionDetector, JndiInjectionDetector,
10    LdapInjectionDetector, NoSqlInjectionDetector, SqlInjectionDetector, SsiInjectionDetector,
11    SstiDetector, XPathInjectionDetector, XssDetector,
12};
13use crate::protocol::{
14    CorsDetector, DnsRebindingDetector, HeaderInjectionDetector, HostHeaderDetector,
15    OpenRedirectDetector, RequestSmugglingDetector, SsrfDetector, WebSocketDetector, XxeDetector,
16};
17use crate::{Detector, result::DetectionResult};
18
19pub struct Scanner {
20    detectors: Vec<Box<dyn Detector>>,
21}
22
23impl Default for Scanner {
24    fn default() -> Self {
25        Self {
26            detectors: vec![
27                // Injection
28                Box::new(XssDetector),
29                Box::new(SqlInjectionDetector),
30                Box::new(CommandInjectionDetector),
31                Box::new(NoSqlInjectionDetector),
32                Box::new(LdapInjectionDetector),
33                Box::new(XPathInjectionDetector),
34                Box::new(JndiInjectionDetector),
35                Box::new(SsiInjectionDetector),
36                Box::new(GraphQlInjectionDetector),
37                Box::new(SstiDetector),
38                // Protocol
39                Box::new(SsrfDetector),
40                Box::new(XxeDetector),
41                Box::new(HeaderInjectionDetector),
42                Box::new(HostHeaderDetector),
43                Box::new(RequestSmugglingDetector),
44                Box::new(OpenRedirectDetector),
45                Box::new(CorsDetector),
46                Box::new(WebSocketDetector),
47                Box::new(DnsRebindingDetector),
48                // Data
49                Box::new(DeserializationDetector),
50                Box::new(CsvInjectionDetector),
51                Box::new(MailHeaderDetector),
52                Box::new(JwtAttackDetector),
53                Box::new(PrototypePollutionDetector),
54                // File
55                Box::new(PathTraversalDetector),
56                Box::new(UploadDetector),
57                Box::new(DataLeakDetector),
58            ],
59        }
60    }
61}
62
63impl Scanner {
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    pub fn builder() -> ScannerBuilder {
69        ScannerBuilder::default()
70    }
71
72    pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
73        let mut results = Vec::new();
74        for detector in &self.detectors {
75            if let Some(result) = detector.detect(input) {
76                results.push(result);
77            }
78        }
79        results
80    }
81
82    pub fn scan_with(&self, input: &str, names: &[&str]) -> Vec<DetectionResult> {
83        let mut results = Vec::new();
84        for detector in &self.detectors {
85            if names.contains(&detector.name())
86                && let Some(result) = detector.detect(input)
87            {
88                results.push(result);
89            }
90        }
91        results
92    }
93}
94
95#[derive(Default)]
96pub struct ScannerBuilder {
97    detectors: Vec<Box<dyn Detector>>,
98}
99
100impl ScannerBuilder {
101    pub fn with_detector(mut self, detector: Box<dyn Detector>) -> Self {
102        self.detectors.push(detector);
103        self
104    }
105
106    pub fn build(self) -> Scanner {
107        Scanner {
108            detectors: self.detectors,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::{AttackCategory, Severity};
117
118    const XSS: &str = "<script>alert(1)</script>";
119
120    fn types(results: &[DetectionResult]) -> Vec<&str> {
121        results.iter().map(|r| r.attack_type.as_str()).collect()
122    }
123
124    #[test]
125    fn default_scanner_registers_all_27_detectors() {
126        assert_eq!(Scanner::default().detectors.len(), 27);
127    }
128
129    #[test]
130    fn scan_detects_known_attack() {
131        let results = Scanner::default().scan(XSS);
132        assert_eq!(results.len(), 1);
133        assert_eq!(results[0].attack_type, "xss");
134        assert_eq!(results[0].category, AttackCategory::Injection);
135        assert_eq!(results[0].severity, Severity::Critical);
136    }
137
138    #[test]
139    fn scan_returns_empty_for_clean_input() {
140        assert!(Scanner::default().scan("hello world 123").is_empty());
141    }
142
143    #[test]
144    fn scan_returns_empty_for_empty_string() {
145        assert!(Scanner::default().scan("").is_empty());
146    }
147
148    #[test]
149    fn scan_with_filters_by_detector_name() {
150        let scanner = Scanner::default();
151        assert_eq!(scanner.scan_with(XSS, &["xss"]).len(), 1);
152        assert!(scanner.scan_with(XSS, &["sql_injection"]).is_empty());
153        assert!(scanner.scan_with(XSS, &["unknown"]).is_empty());
154    }
155
156    #[test]
157    fn scan_with_multiple_names() {
158        let input = "1 UNION SELECT password FROM users; <script>alert(1)</script>";
159        let results = Scanner::default().scan_with(input, &["sql_injection", "xss"]);
160        let t = types(&results);
161        assert!(t.contains(&"sql_injection") && t.contains(&"xss"));
162    }
163
164    #[test]
165    fn new_matches_default() {
166        assert_eq!(
167            Scanner::new().detectors.len(),
168            Scanner::default().detectors.len()
169        );
170    }
171
172    #[test]
173    fn builder_without_detectors_scans_nothing() {
174        assert!(Scanner::builder().build().scan(XSS).is_empty());
175    }
176
177    #[test]
178    fn builder_with_custom_detector() {
179        let scanner = Scanner::builder()
180            .with_detector(Box::new(crate::injection::XssDetector))
181            .build();
182        let results = scanner.scan(XSS);
183        assert_eq!(results.len(), 1);
184        assert_eq!(results[0].attack_type, "xss");
185    }
186
187    #[test]
188    fn detection_result_has_pattern_offset_and_message() {
189        let results = Scanner::default().scan(XSS);
190        let r = &results[0];
191        assert!(!r.matched_pattern.is_empty());
192        assert!(r.offset <= XSS.len());
193        assert!(!r.message.is_empty());
194    }
195
196    #[test]
197    fn scan_is_deterministic() {
198        let input = "SELECT 1; <script>alert(1)</script>";
199        assert_eq!(
200            Scanner::default().scan(input),
201            Scanner::default().scan(input)
202        );
203    }
204}