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>";
131
132 fn types(results: &[DetectionResult]) -> Vec<&str> {
133 results.iter().map(|r| r.attack_type.as_str()).collect()
134 }
135
136 #[test]
137 fn default_scanner_registers_all_32_detectors() {
138 assert_eq!(Scanner::default().detectors.len(), 32);
139 }
140
141 #[test]
142 fn default_scanner_registers_each_detector_once() {
143 let scanner = Scanner::default();
144 let mut names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
145 names.sort_unstable();
146 let before = names.len();
147 names.dedup();
148 assert_eq!(names.len(), before, "检测器名字重复: {names:?}");
149 }
150
151 #[test]
152 fn new_detectors_are_registered() {
153 let scanner = Scanner::default();
154 let names: Vec<&str> = scanner.detectors.iter().map(|d| d.name()).collect();
155 for expected in [
156 "log4shell",
157 "hpp",
158 "formula_injection",
159 "redos",
160 "format_string",
161 ] {
162 assert!(names.contains(&expected), "缺少检测器: {expected}");
163 }
164 }
165
166 #[test]
170 fn new_detectors_only_do_not_fire_on_clean_input() {
171 let scanner = Scanner::default();
172 for input in [
173 "the price is ${amount}",
174 "a=1&b=2",
175 "= 5",
176 "-3 度",
177 "a@b.com",
178 "100% safe",
179 "50% off",
180 "line one\r\nline two",
181 "5*(3+2)",
182 ] {
183 let results = scanner.scan_with(
184 input,
185 &[
186 "log4shell",
187 "hpp",
188 "formula_injection",
189 "redos",
190 "format_string",
191 ],
192 );
193 assert!(
194 results.is_empty(),
195 "新检测器误报 {input:?}: {:?}",
196 types(&results)
197 );
198 }
199 }
200
201 #[test]
202 fn assess_returns_none_for_clean_input() {
203 let a = Scanner::default().assess("hello world 123");
204 assert_eq!(a.level, crate::score::RiskLevel::None);
205 assert_eq!(a.score, 0);
206 assert_eq!(a.results, 0);
207 }
208
209 #[test]
210 fn assess_returns_critical_for_critical_hit() {
211 let a = Scanner::default().assess(XSS);
212 assert_eq!(a.level, crate::score::RiskLevel::Critical);
213 assert_eq!(a.results, 1);
214 assert!(a.score > 0);
215 }
216
217 #[test]
218 fn assess_escalates_on_stacked_medium_hits() {
219 let input = "=cmd|' /C calc'!A0 `cat /etc/passwd` ../../../etc/passwd";
220 let a = Scanner::default().assess(input);
221 assert!(a.results >= 3, "期望多条命中,实际 {:?}", a);
222 assert!(
223 a.level >= crate::score::RiskLevel::High,
224 "叠加后应升级: {:?}",
225 a
226 );
227 }
228
229 #[test]
230 fn scan_detects_known_attack() {
231 let results = Scanner::default().scan(XSS);
232 assert_eq!(results.len(), 1);
233 assert_eq!(results[0].attack_type, "xss");
234 assert_eq!(results[0].category, AttackCategory::Injection);
235 assert_eq!(results[0].severity, Severity::Critical);
236 }
237
238 #[test]
239 fn scan_returns_empty_for_clean_input() {
240 for input in [
241 "hello world 123",
242 "q=2024--2025",
243 "q=donation=5",
244 "穿越之霸道总裁爱上我--重生之都市修仙",
245 ] {
246 assert!(
247 Scanner::default().scan(input).is_empty(),
248 "false positive: {input}"
249 );
250 }
251 }
252
253 #[test]
254 fn scan_returns_empty_for_empty_string() {
255 assert!(Scanner::default().scan("").is_empty());
256 }
257
258 #[test]
259 fn scan_with_filters_by_detector_name() {
260 let scanner = Scanner::default();
261 assert_eq!(scanner.scan_with(XSS, &["xss"]).len(), 1);
262 assert!(scanner.scan_with(XSS, &["sql_injection"]).is_empty());
263 assert!(scanner.scan_with(XSS, &["unknown"]).is_empty());
264 }
265
266 #[test]
267 fn scan_with_multiple_names() {
268 let input = "1 UNION SELECT password FROM users; <script>alert(1)</script>";
269 let results = Scanner::default().scan_with(input, &["sql_injection", "xss"]);
270 let t = types(&results);
271 assert!(t.contains(&"sql_injection") && t.contains(&"xss"));
272 }
273
274 #[test]
275 fn new_matches_default() {
276 assert_eq!(
277 Scanner::new().detectors.len(),
278 Scanner::default().detectors.len()
279 );
280 }
281
282 #[test]
283 fn builder_without_detectors_scans_nothing() {
284 assert!(Scanner::builder().build().scan(XSS).is_empty());
285 }
286
287 #[test]
288 fn builder_with_custom_detector() {
289 let scanner = Scanner::builder()
290 .with_detector(Box::new(crate::injection::XssDetector))
291 .build();
292 let results = scanner.scan(XSS);
293 assert_eq!(results.len(), 1);
294 assert_eq!(results[0].attack_type, "xss");
295 }
296
297 #[test]
298 fn detection_result_has_pattern_offset_and_message() {
299 let results = Scanner::default().scan(XSS);
300 let r = &results[0];
301 assert!(!r.matched_pattern.is_empty());
302 assert!(r.offset <= XSS.len());
303 assert!(!r.message.is_empty());
304 }
305
306 #[test]
307 fn scan_is_deterministic() {
308 let input = "SELECT 1; <script>alert(1)</script>";
309 assert_eq!(
310 Scanner::default().scan(input),
311 Scanner::default().scan(input)
312 );
313 }
314}