1#[derive(Debug, Clone, PartialEq)]
3pub struct Probe {
4 pub payload: String,
6 pub tests: ProbeTarget,
8 pub description: String,
10 pub expected_blocked: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ProbeTarget {
17 SqlKeyword(String),
19 SqlOperator(String),
21 SqlComment(String),
23 SqlQuote,
25 SqlTautology(String),
27 XssTag(String),
29 XssEvent(String),
31 XssExecFunction(String),
33 CmdSeparator(String),
35 CmdCommand(String),
37 CmdPath(String),
39 Baseline,
41}
42
43#[must_use]
45pub fn generate_probes() -> Vec<Probe> {
46 let mut probes = Vec::new();
47 probes.push(baseline_probe("test_value_12345", "baseline benign value"));
48 probes.extend(sql_keyword_probes());
49 probes.extend(sql_operator_probes());
50 probes.extend(sql_comment_probes());
51 probes.push(Probe {
52 payload: "'".into(),
53 tests: ProbeTarget::SqlQuote,
54 description: "SQL single quote".into(),
55 expected_blocked: true,
56 });
57 probes.extend(sql_tautology_probes());
58 probes.extend(xss_tag_probes());
59 probes.extend(xss_event_probes());
60 probes.extend(xss_function_probes());
61 probes.extend(command_separator_probes());
62 probes.extend(command_name_probes());
63 probes.extend(command_path_probes());
64 probes
65}
66
67pub(crate) fn baseline_probe(payload: &str, description: &str) -> Probe {
68 Probe {
69 payload: payload.into(),
70 tests: ProbeTarget::Baseline,
71 description: description.into(),
72 expected_blocked: false,
73 }
74}
75
76pub(crate) fn sql_keyword_probes() -> Vec<Probe> {
77 build_probes(
78 &[
79 "SELECT",
80 "UNION",
81 "INSERT",
82 "UPDATE",
83 "DELETE",
84 "DROP",
85 "FROM",
86 "WHERE",
87 "ORDER BY",
88 "GROUP BY",
89 "HAVING",
90 "SLEEP",
91 "BENCHMARK",
92 "WAITFOR",
93 ],
94 |keyword| Probe {
95 payload: format!("test {keyword} value"),
96 tests: ProbeTarget::SqlKeyword(keyword.to_string()),
97 description: format!("SQL keyword: {keyword}"),
98 expected_blocked: true,
99 },
100 )
101}
102
103pub(crate) fn sql_operator_probes() -> Vec<Probe> {
104 build_probes(
105 &[
106 "=", "!=", "<>", "LIKE", "IN(", "BETWEEN", "IS NULL", "REGEXP",
107 ],
108 |operator| Probe {
109 payload: format!("test{operator}test"),
110 tests: ProbeTarget::SqlOperator(operator.to_string()),
111 description: format!("SQL operator: {operator}"),
112 expected_blocked: true,
113 },
114 )
115}
116
117pub(crate) fn sql_comment_probes() -> Vec<Probe> {
118 build_probes(&["--", "#", "/***/", "-- -", "--+"], |comment| Probe {
119 payload: format!("test{comment}test"),
120 tests: ProbeTarget::SqlComment(comment.to_string()),
121 description: format!("SQL comment: {comment}"),
122 expected_blocked: true,
123 })
124}
125
126pub(crate) fn sql_tautology_probes() -> Vec<Probe> {
127 build_probes(
128 &[
129 "1=1",
130 "1 LIKE 1",
131 "'a'='a'",
132 "1 BETWEEN 0 AND 2",
133 "1 IN(1)",
134 "true",
135 ],
136 |tautology| Probe {
137 payload: tautology.to_string(),
138 tests: ProbeTarget::SqlTautology(tautology.to_string()),
139 description: format!("SQL tautology: {tautology}"),
140 expected_blocked: true,
141 },
142 )
143}
144
145pub(crate) fn xss_tag_probes() -> Vec<Probe> {
146 [
147 ("script", "<script>", true),
148 ("img", "<img src=x>", false),
149 ("svg", "<svg>", false),
150 ("iframe", "<iframe>", true),
151 ("body", "<body>", false),
152 ("details", "<details>", false),
153 ("input", "<input>", false),
154 ("marquee", "<marquee>", false),
155 ("video", "<video>", false),
156 ("object", "<object>", false),
157 ("math", "<math>", false),
158 ("style", "<style>", false),
159 ]
160 .into_iter()
161 .map(|(name, payload, expected_blocked)| Probe {
162 payload: payload.into(),
163 tests: ProbeTarget::XssTag(name.into()),
164 description: format!("XSS tag: {name}"),
165 expected_blocked,
166 })
167 .collect()
168}
169
170pub(crate) fn xss_event_probes() -> Vec<Probe> {
171 build_probes(
172 &[
173 "onerror",
174 "onload",
175 "onclick",
176 "onfocus",
177 "onmouseover",
178 "ontoggle",
179 "onbegin",
180 "onstart",
181 "onsubmit",
182 ],
183 |event| Probe {
184 payload: format!("<x {event}=1>"),
185 tests: ProbeTarget::XssEvent(event.to_string()),
186 description: format!("XSS event: {event}"),
187 expected_blocked: true,
188 },
189 )
190}
191
192pub(crate) fn xss_function_probes() -> Vec<Probe> {
193 [
194 ("alert", "alert(1)", true),
195 ("confirm", "confirm(1)", false),
196 ("prompt", "prompt(1)", false),
197 ("eval", "eval('x')", true),
198 ("Function", "Function('x')()", false),
199 ("constructor", "[].constructor.constructor('x')()", false),
200 ("setTimeout", "setTimeout('x')", false),
201 ]
202 .into_iter()
203 .map(|(name, payload, expected_blocked)| Probe {
204 payload: payload.into(),
205 tests: ProbeTarget::XssExecFunction(name.into()),
206 description: format!("XSS function: {name}"),
207 expected_blocked,
208 })
209 .collect()
210}
211
212pub(crate) fn command_separator_probes() -> Vec<Probe> {
213 build_probes(&[";", "|", "||", "&&", "`", "$("], |separator| Probe {
214 payload: format!("test{separator}test"),
215 tests: ProbeTarget::CmdSeparator(separator.to_string()),
216 description: format!("CMD separator: {separator}"),
217 expected_blocked: true,
218 })
219}
220
221pub(crate) fn command_name_probes() -> Vec<Probe> {
222 build_probes(
223 &["cat", "ls", "id", "whoami", "wget", "curl", "ping", "nc"],
224 |command| Probe {
225 payload: command.to_string(),
226 tests: ProbeTarget::CmdCommand(command.to_string()),
227 description: format!("CMD command: {command}"),
228 expected_blocked: false,
229 },
230 )
231}
232
233pub(crate) fn command_path_probes() -> Vec<Probe> {
234 build_probes(
235 &[
236 "/etc/passwd",
237 "/etc/shadow",
238 "/proc/self/environ",
239 "/bin/sh",
240 ],
241 |path| Probe {
242 payload: path.to_string(),
243 tests: ProbeTarget::CmdPath(path.to_string()),
244 description: format!("CMD path: {path}"),
245 expected_blocked: true,
246 },
247 )
248}
249
250fn build_probes<T, F>(items: &[T], builder: F) -> Vec<Probe>
251where
252 T: Copy,
253 F: Fn(T) -> Probe,
254{
255 items.iter().copied().map(builder).collect()
256}
257
258#[cfg(test)]
259mod tests {
260 use super::{ProbeTarget, generate_probes};
261
262 #[test]
263 fn generate_probes_has_baseline() {
264 let probes = generate_probes();
265 assert!(
266 probes
267 .iter()
268 .any(|probe| probe.tests == ProbeTarget::Baseline)
269 );
270 }
271
272 #[test]
273 fn generate_probes_covers_all_categories() {
274 let probes = generate_probes();
275 assert!(
276 probes
277 .iter()
278 .any(|probe| matches!(probe.tests, ProbeTarget::SqlKeyword(_)))
279 );
280 assert!(
281 probes
282 .iter()
283 .any(|probe| matches!(probe.tests, ProbeTarget::SqlOperator(_)))
284 );
285 assert!(
286 probes
287 .iter()
288 .any(|probe| matches!(probe.tests, ProbeTarget::SqlComment(_)))
289 );
290 assert!(
291 probes
292 .iter()
293 .any(|probe| matches!(probe.tests, ProbeTarget::SqlQuote))
294 );
295 assert!(
296 probes
297 .iter()
298 .any(|probe| matches!(probe.tests, ProbeTarget::SqlTautology(_)))
299 );
300 assert!(
301 probes
302 .iter()
303 .any(|probe| matches!(probe.tests, ProbeTarget::XssTag(_)))
304 );
305 assert!(
306 probes
307 .iter()
308 .any(|probe| matches!(probe.tests, ProbeTarget::XssEvent(_)))
309 );
310 assert!(
311 probes
312 .iter()
313 .any(|probe| matches!(probe.tests, ProbeTarget::XssExecFunction(_)))
314 );
315 assert!(
316 probes
317 .iter()
318 .any(|probe| matches!(probe.tests, ProbeTarget::CmdSeparator(_)))
319 );
320 assert!(
321 probes
322 .iter()
323 .any(|probe| matches!(probe.tests, ProbeTarget::CmdCommand(_)))
324 );
325 assert!(
326 probes
327 .iter()
328 .any(|probe| matches!(probe.tests, ProbeTarget::CmdPath(_)))
329 );
330 }
331
332 #[test]
333 fn generate_probes_has_many() {
334 let probes = generate_probes();
335 assert!(
336 probes.len() >= 60,
337 "expected 60+ probes, got {}",
338 probes.len()
339 );
340 }
341
342 #[test]
343 fn probes_have_descriptions() {
344 let probes = generate_probes();
345 for probe in &probes {
346 assert!(
347 !probe.description.is_empty(),
348 "probe should have description"
349 );
350 assert!(
351 !probe.payload.is_empty() || probe.tests == ProbeTarget::Baseline,
352 "probe should have payload"
353 );
354 }
355 }
356
357 #[test]
358 fn sql_quote_expected_blocked() {
359 let probes = generate_probes();
360 let quote = probes
361 .iter()
362 .find(|p| matches!(p.tests, ProbeTarget::SqlQuote));
363 assert!(quote.is_some());
364 assert!(
365 quote.unwrap().expected_blocked,
366 "SQL quote should be expected blocked"
367 );
368 }
369}