1use crate::data::{
4 CsvInjectionDetector, DeserializationDetector, FormulaInjectionDetector, JwtAttackDetector,
5 MailHeaderDetector, PrototypePollutionDetector, ReDoSDetector,
6};
7use crate::file::{DataLeakDetector, PathTraversalDetector, UploadDetector};
8use crate::injection::{
9 CommandInjectionDetector, FormatStringDetector, GraphQlInjectionDetector,
10 JndiInjectionDetector, LdapInjectionDetector, NoSqlInjectionDetector, SqlInjectionDetector,
11 SsiInjectionDetector, SstiDetector, XPathInjectionDetector, XssDetector,
12};
13use crate::protocol::{
14 CorsDetector, DnsRebindingDetector, HeaderInjectionDetector, HostHeaderDetector,
15 HttpParameterPollutionDetector, Log4ShellDetector, OpenRedirectDetector,
16 RequestSmugglingDetector, SsrfDetector, WebSocketDetector, XxeDetector,
17};
18use crate::score::{self, RiskAssessment};
19use crate::{Detector, result::DetectionResult};
20
21pub struct Scanner {
44 detectors: Vec<Box<dyn Detector>>,
45}
46
47impl Default for Scanner {
48 fn default() -> Self {
49 Self {
50 detectors: vec![
51 Box::new(XssDetector),
53 Box::new(SqlInjectionDetector),
54 Box::new(CommandInjectionDetector),
55 Box::new(NoSqlInjectionDetector),
56 Box::new(LdapInjectionDetector),
57 Box::new(XPathInjectionDetector),
58 Box::new(JndiInjectionDetector),
59 Box::new(SsiInjectionDetector),
60 Box::new(GraphQlInjectionDetector),
61 Box::new(SstiDetector),
62 Box::new(FormatStringDetector),
63 Box::new(SsrfDetector),
65 Box::new(XxeDetector),
66 Box::new(HeaderInjectionDetector),
67 Box::new(HostHeaderDetector),
68 Box::new(RequestSmugglingDetector),
69 Box::new(OpenRedirectDetector),
70 Box::new(CorsDetector),
71 Box::new(WebSocketDetector),
72 Box::new(DnsRebindingDetector),
73 Box::new(Log4ShellDetector),
74 Box::new(HttpParameterPollutionDetector),
75 Box::new(DeserializationDetector),
77 Box::new(CsvInjectionDetector),
78 Box::new(MailHeaderDetector),
79 Box::new(JwtAttackDetector),
80 Box::new(PrototypePollutionDetector),
81 Box::new(FormulaInjectionDetector),
82 Box::new(ReDoSDetector),
83 Box::new(PathTraversalDetector),
85 Box::new(UploadDetector),
86 Box::new(DataLeakDetector),
87 ],
88 }
89 }
90}
91
92impl Scanner {
93 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn builder() -> ScannerBuilder {
101 ScannerBuilder::default()
102 }
103
104 #[must_use]
114 pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
115 let mut results = Vec::new();
116 for detector in &self.detectors {
117 if let Some(result) = detector.detect(input) {
118 results.push(result);
119 }
120 }
121 results
122 }
123
124 #[must_use]
132 pub fn assess(&self, input: &str) -> RiskAssessment {
133 score::assess(&self.scan(input))
134 }
135
136 #[must_use]
145 pub fn scan_with(&self, input: &str, names: &[&str]) -> Vec<DetectionResult> {
146 let mut results = Vec::new();
147 for detector in &self.detectors {
148 if names.contains(&detector.name())
149 && let Some(result) = detector.detect(input)
150 {
151 results.push(result);
152 }
153 }
154 results
155 }
156
157 pub fn detector_names(&self) -> impl Iterator<Item = &'static str> + '_ {
166 self.detectors.iter().map(|d| d.name())
167 }
168}
169
170#[derive(Default)]
176pub struct ScannerBuilder {
177 detectors: Vec<Box<dyn Detector>>,
178}
179
180impl ScannerBuilder {
181 #[must_use]
186 pub fn with_detector(mut self, detector: Box<dyn Detector>) -> Self {
187 self.detectors.push(detector);
188 self
189 }
190
191 pub fn build(self) -> Scanner {
193 Scanner {
194 detectors: self.detectors,
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::{AttackCategory, Severity};
203
204 const XSS: &str = "<script>alert(1)</script>";
206
207 const XSS_CRITICAL: &str = "<img src=x onerror=alert(1)>";
210
211 fn types(results: &[DetectionResult]) -> Vec<&str> {
212 results.iter().map(|r| r.attack_type.as_str()).collect()
213 }
214
215 #[test]
216 fn default_scanner_registers_all_32_detectors() {
217 assert_eq!(Scanner::default().detectors.len(), 32);
218 }
219
220 #[test]
221 fn default_scanner_registers_each_detector_once() {
222 let scanner = Scanner::default();
223 let mut names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
224 names.sort_unstable();
225 let before = names.len();
226 names.dedup();
227 assert_eq!(names.len(), before, "检测器名字重复: {names:?}");
228 }
229
230 #[test]
231 fn new_detectors_are_registered() {
232 let scanner = Scanner::default();
233 let names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
234 for expected in [
235 "log4shell",
236 "hpp",
237 "formula_injection",
238 "redos",
239 "format_string",
240 ] {
241 assert!(names.contains(&expected), "缺少检测器: {expected}");
242 }
243 }
244
245 #[test]
249 fn new_detectors_only_do_not_fire_on_clean_input() {
250 let scanner = Scanner::default();
251 for input in [
252 "the price is ${amount}",
253 "a=1&b=2",
254 "= 5",
255 "-3 度",
256 "a@b.com",
257 "100% safe",
258 "50% off",
259 "line one\r\nline two",
260 "5*(3+2)",
261 ] {
262 let results = scanner.scan_with(
263 input,
264 &[
265 "log4shell",
266 "hpp",
267 "formula_injection",
268 "redos",
269 "format_string",
270 ],
271 );
272 assert!(
273 results.is_empty(),
274 "新检测器误报 {input:?}: {:?}",
275 types(&results)
276 );
277 }
278 }
279
280 #[test]
281 fn assess_returns_none_for_clean_input() {
282 let a = Scanner::default().assess("hello world 123");
283 assert_eq!(a.level, crate::score::RiskLevel::None);
284 assert_eq!(a.score, 0);
285 assert_eq!(a.results, 0);
286 }
287
288 #[test]
289 fn assess_returns_critical_for_critical_hit() {
290 let a = Scanner::default().assess(XSS_CRITICAL);
291 assert_eq!(a.level, crate::score::RiskLevel::Critical);
292 assert_eq!(a.results, 1);
293 assert!(a.score > 0);
294 }
295
296 #[test]
300 fn tag_presence_is_low_while_event_handler_is_critical() {
301 let scanner = Scanner::default();
302
303 let weak = scanner.scan(XSS);
304 assert_eq!(weak.len(), 1);
305 assert_eq!(weak[0].attack_type, "xss");
306 assert_eq!(weak[0].severity, Severity::Low);
307 assert!(
308 scanner.assess(XSS).level < crate::score::RiskLevel::High,
309 "标签存在不得单独越线"
310 );
311
312 let strong = scanner.scan(XSS_CRITICAL);
313 assert_eq!(strong[0].severity, Severity::Critical);
314 assert_eq!(
315 scanner.assess(XSS_CRITICAL).level,
316 crate::score::RiskLevel::Critical
317 );
318 }
319
320 #[test]
321 fn assess_escalates_on_stacked_medium_hits() {
322 let input = "=cmd|' /C calc'!A0 `cat /etc/passwd` ../../../etc/passwd";
323 let a = Scanner::default().assess(input);
324 assert!(a.results >= 3, "期望多条命中,实际 {:?}", a);
325 assert!(
326 a.level >= crate::score::RiskLevel::High,
327 "叠加后应升级: {:?}",
328 a
329 );
330 }
331
332 #[test]
333 fn scan_detects_known_attack() {
334 let results = Scanner::default().scan(XSS_CRITICAL);
335 assert_eq!(results.len(), 1);
336 assert_eq!(results[0].attack_type, "xss");
337 assert_eq!(results[0].category, AttackCategory::Injection);
338 assert_eq!(results[0].severity, Severity::Critical);
339 }
340
341 #[test]
342 fn scan_returns_empty_for_clean_input() {
343 for input in [
344 "hello world 123",
345 "q=2024--2025",
346 "q=donation=5",
347 "穿越之霸道总裁爱上我--重生之都市修仙",
348 ] {
349 assert!(
350 Scanner::default().scan(input).is_empty(),
351 "false positive: {input}"
352 );
353 }
354 }
355
356 #[test]
357 fn scan_returns_empty_for_empty_string() {
358 assert!(Scanner::default().scan("").is_empty());
359 }
360
361 #[test]
362 fn scan_with_filters_by_detector_name() {
363 let scanner = Scanner::default();
364 assert_eq!(scanner.scan_with(XSS, &["xss"]).len(), 1);
365 assert!(scanner.scan_with(XSS, &["sql_injection"]).is_empty());
366 assert!(scanner.scan_with(XSS, &["unknown"]).is_empty());
367 }
368
369 #[test]
370 fn scan_with_multiple_names() {
371 let input = "1 UNION SELECT password FROM users; <script>alert(1)</script>";
372 let results = Scanner::default().scan_with(input, &["sql_injection", "xss"]);
373 let t = types(&results);
374 assert!(t.contains(&"sql_injection") && t.contains(&"xss"));
375 }
376
377 #[test]
378 fn new_matches_default() {
379 assert_eq!(
380 Scanner::new().detectors.len(),
381 Scanner::default().detectors.len()
382 );
383 }
384
385 #[test]
386 fn builder_without_detectors_scans_nothing() {
387 assert!(Scanner::builder().build().scan(XSS).is_empty());
388 }
389
390 #[test]
391 fn builder_with_custom_detector() {
392 let scanner = Scanner::builder()
393 .with_detector(Box::new(crate::injection::XssDetector))
394 .build();
395 let results = scanner.scan(XSS);
396 assert_eq!(results.len(), 1);
397 assert_eq!(results[0].attack_type, "xss");
398 }
399
400 #[test]
401 fn detection_result_has_pattern_offset_and_message() {
402 let results = Scanner::default().scan(XSS);
403 let r = &results[0];
404 assert!(!r.matched_pattern.is_empty());
405 assert!(r.offset <= XSS.len());
406 assert!(!r.message.is_empty());
407 }
408
409 #[test]
410 fn scan_is_deterministic() {
411 let input = "SELECT 1; <script>alert(1)</script>";
412 assert_eq!(
413 Scanner::default().scan(input),
414 Scanner::default().scan(input)
415 );
416 }
417}