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 {
22 detectors: Vec<Box<dyn Detector>>,
23}
24
25impl Default for Scanner {
26 fn default() -> Self {
27 Self {
28 detectors: vec![
29 Box::new(XssDetector),
31 Box::new(SqlInjectionDetector),
32 Box::new(CommandInjectionDetector),
33 Box::new(NoSqlInjectionDetector),
34 Box::new(LdapInjectionDetector),
35 Box::new(XPathInjectionDetector),
36 Box::new(JndiInjectionDetector),
37 Box::new(SsiInjectionDetector),
38 Box::new(GraphQlInjectionDetector),
39 Box::new(SstiDetector),
40 Box::new(FormatStringDetector),
41 Box::new(SsrfDetector),
43 Box::new(XxeDetector),
44 Box::new(HeaderInjectionDetector),
45 Box::new(HostHeaderDetector),
46 Box::new(RequestSmugglingDetector),
47 Box::new(OpenRedirectDetector),
48 Box::new(CorsDetector),
49 Box::new(WebSocketDetector),
50 Box::new(DnsRebindingDetector),
51 Box::new(Log4ShellDetector),
52 Box::new(HttpParameterPollutionDetector),
53 Box::new(DeserializationDetector),
55 Box::new(CsvInjectionDetector),
56 Box::new(MailHeaderDetector),
57 Box::new(JwtAttackDetector),
58 Box::new(PrototypePollutionDetector),
59 Box::new(FormulaInjectionDetector),
60 Box::new(ReDoSDetector),
61 Box::new(PathTraversalDetector),
63 Box::new(UploadDetector),
64 Box::new(DataLeakDetector),
65 ],
66 }
67 }
68}
69
70impl Scanner {
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn builder() -> ScannerBuilder {
76 ScannerBuilder::default()
77 }
78
79 pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
80 let mut results = Vec::new();
81 for detector in &self.detectors {
82 if let Some(result) = detector.detect(input) {
83 results.push(result);
84 }
85 }
86 results
87 }
88
89 pub fn assess(&self, input: &str) -> RiskAssessment {
91 score::assess(&self.scan(input))
92 }
93
94 pub fn scan_with(&self, input: &str, names: &[&str]) -> Vec<DetectionResult> {
95 let mut results = Vec::new();
96 for detector in &self.detectors {
97 if names.contains(&detector.name())
98 && let Some(result) = detector.detect(input)
99 {
100 results.push(result);
101 }
102 }
103 results
104 }
105}
106
107#[derive(Default)]
108pub struct ScannerBuilder {
109 detectors: Vec<Box<dyn Detector>>,
110}
111
112impl ScannerBuilder {
113 pub fn with_detector(mut self, detector: Box<dyn Detector>) -> Self {
114 self.detectors.push(detector);
115 self
116 }
117
118 pub fn build(self) -> Scanner {
119 Scanner {
120 detectors: self.detectors,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::{AttackCategory, Severity};
129
130 const XSS: &str = "<script>alert(1)</script>";
132
133 const XSS_CRITICAL: &str = "<img src=x onerror=alert(1)>";
136
137 fn types(results: &[DetectionResult]) -> Vec<&str> {
138 results.iter().map(|r| r.attack_type.as_str()).collect()
139 }
140
141 #[test]
142 fn default_scanner_registers_all_32_detectors() {
143 assert_eq!(Scanner::default().detectors.len(), 32);
144 }
145
146 #[test]
147 fn default_scanner_registers_each_detector_once() {
148 let scanner = Scanner::default();
149 let mut names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
150 names.sort_unstable();
151 let before = names.len();
152 names.dedup();
153 assert_eq!(names.len(), before, "检测器名字重复: {names:?}");
154 }
155
156 #[test]
157 fn new_detectors_are_registered() {
158 let scanner = Scanner::default();
159 let names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
160 for expected in [
161 "log4shell",
162 "hpp",
163 "formula_injection",
164 "redos",
165 "format_string",
166 ] {
167 assert!(names.contains(&expected), "缺少检测器: {expected}");
168 }
169 }
170
171 #[test]
175 fn new_detectors_only_do_not_fire_on_clean_input() {
176 let scanner = Scanner::default();
177 for input in [
178 "the price is ${amount}",
179 "a=1&b=2",
180 "= 5",
181 "-3 度",
182 "a@b.com",
183 "100% safe",
184 "50% off",
185 "line one\r\nline two",
186 "5*(3+2)",
187 ] {
188 let results = scanner.scan_with(
189 input,
190 &[
191 "log4shell",
192 "hpp",
193 "formula_injection",
194 "redos",
195 "format_string",
196 ],
197 );
198 assert!(
199 results.is_empty(),
200 "新检测器误报 {input:?}: {:?}",
201 types(&results)
202 );
203 }
204 }
205
206 #[test]
207 fn assess_returns_none_for_clean_input() {
208 let a = Scanner::default().assess("hello world 123");
209 assert_eq!(a.level, crate::score::RiskLevel::None);
210 assert_eq!(a.score, 0);
211 assert_eq!(a.results, 0);
212 }
213
214 #[test]
215 fn assess_returns_critical_for_critical_hit() {
216 let a = Scanner::default().assess(XSS_CRITICAL);
217 assert_eq!(a.level, crate::score::RiskLevel::Critical);
218 assert_eq!(a.results, 1);
219 assert!(a.score > 0);
220 }
221
222 #[test]
226 fn tag_presence_is_low_while_event_handler_is_critical() {
227 let scanner = Scanner::default();
228
229 let weak = scanner.scan(XSS);
230 assert_eq!(weak.len(), 1);
231 assert_eq!(weak[0].attack_type, "xss");
232 assert_eq!(weak[0].severity, Severity::Low);
233 assert!(
234 scanner.assess(XSS).level < crate::score::RiskLevel::High,
235 "标签存在不得单独越线"
236 );
237
238 let strong = scanner.scan(XSS_CRITICAL);
239 assert_eq!(strong[0].severity, Severity::Critical);
240 assert_eq!(
241 scanner.assess(XSS_CRITICAL).level,
242 crate::score::RiskLevel::Critical
243 );
244 }
245
246 #[test]
247 fn assess_escalates_on_stacked_medium_hits() {
248 let input = "=cmd|' /C calc'!A0 `cat /etc/passwd` ../../../etc/passwd";
249 let a = Scanner::default().assess(input);
250 assert!(a.results >= 3, "期望多条命中,实际 {:?}", a);
251 assert!(
252 a.level >= crate::score::RiskLevel::High,
253 "叠加后应升级: {:?}",
254 a
255 );
256 }
257
258 #[test]
259 fn scan_detects_known_attack() {
260 let results = Scanner::default().scan(XSS_CRITICAL);
261 assert_eq!(results.len(), 1);
262 assert_eq!(results[0].attack_type, "xss");
263 assert_eq!(results[0].category, AttackCategory::Injection);
264 assert_eq!(results[0].severity, Severity::Critical);
265 }
266
267 #[test]
268 fn scan_returns_empty_for_clean_input() {
269 for input in [
270 "hello world 123",
271 "q=2024--2025",
272 "q=donation=5",
273 "穿越之霸道总裁爱上我--重生之都市修仙",
274 ] {
275 assert!(
276 Scanner::default().scan(input).is_empty(),
277 "false positive: {input}"
278 );
279 }
280 }
281
282 #[test]
283 fn scan_returns_empty_for_empty_string() {
284 assert!(Scanner::default().scan("").is_empty());
285 }
286
287 #[test]
288 fn scan_with_filters_by_detector_name() {
289 let scanner = Scanner::default();
290 assert_eq!(scanner.scan_with(XSS, &["xss"]).len(), 1);
291 assert!(scanner.scan_with(XSS, &["sql_injection"]).is_empty());
292 assert!(scanner.scan_with(XSS, &["unknown"]).is_empty());
293 }
294
295 #[test]
296 fn scan_with_multiple_names() {
297 let input = "1 UNION SELECT password FROM users; <script>alert(1)</script>";
298 let results = Scanner::default().scan_with(input, &["sql_injection", "xss"]);
299 let t = types(&results);
300 assert!(t.contains(&"sql_injection") && t.contains(&"xss"));
301 }
302
303 #[test]
304 fn new_matches_default() {
305 assert_eq!(
306 Scanner::new().detectors.len(),
307 Scanner::default().detectors.len()
308 );
309 }
310
311 #[test]
312 fn builder_without_detectors_scans_nothing() {
313 assert!(Scanner::builder().build().scan(XSS).is_empty());
314 }
315
316 #[test]
317 fn builder_with_custom_detector() {
318 let scanner = Scanner::builder()
319 .with_detector(Box::new(crate::injection::XssDetector))
320 .build();
321 let results = scanner.scan(XSS);
322 assert_eq!(results.len(), 1);
323 assert_eq!(results[0].attack_type, "xss");
324 }
325
326 #[test]
327 fn detection_result_has_pattern_offset_and_message() {
328 let results = Scanner::default().scan(XSS);
329 let r = &results[0];
330 assert!(!r.matched_pattern.is_empty());
331 assert!(r.offset <= XSS.len());
332 assert!(!r.message.is_empty());
333 }
334
335 #[test]
336 fn scan_is_deterministic() {
337 let input = "SELECT 1; <script>alert(1)</script>";
338 assert_eq!(
339 Scanner::default().scan(input),
340 Scanner::default().scan(input)
341 );
342 }
343}