1use 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 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 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 Box::new(DeserializationDetector),
50 Box::new(CsvInjectionDetector),
51 Box::new(MailHeaderDetector),
52 Box::new(JwtAttackDetector),
53 Box::new(PrototypePollutionDetector),
54 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}