1use std::thread;
5
6use crate::attacks::http::HttpClient;
7use crate::klr::ast::{Attack, Expectation, Value};
8use crate::klr::interpreter::{Interpreter, RunConfig};
9use crate::results::AttackOutcome;
10
11pub fn expand_all(attacks: &[Attack]) -> Vec<Attack> {
14 attacks.iter().flat_map(expand_attack).collect()
15}
16
17pub fn expand_attack(attack: &Attack) -> Vec<Attack> {
19 let mut base = attack.clone();
21 for c in &attack.checks {
22 base.expectations.extend(check_expectations(c));
23 }
24 base.checks.clear();
25
26 if base.mutations.is_empty() {
28 return vec![base];
29 }
30 let mutations = std::mem::take(&mut base.mutations);
31 let mut variants = Vec::new();
32 for m in &mutations {
33 for generator in &m.generators {
34 let values = crate::fuzz::generate(generator);
35 let multi = values.len() > 1;
36 for (i, val) in values.into_iter().enumerate() {
37 let mut v = base.clone();
38 set_send_field(&mut v.send, &m.field, &val);
39 let label = if multi {
40 format!("{generator}#{}", i + 1)
41 } else {
42 generator.clone()
43 };
44 v.name = format!("{} [{}={}]", base.name, m.field, label);
45 variants.push(v);
46 }
47 }
48 }
49 if variants.is_empty() {
50 vec![base]
51 } else {
52 variants
53 }
54}
55
56pub fn run_all<C: HttpClient + Sync>(
61 attacks: &[Attack],
62 client: &C,
63 config: &RunConfig,
64 workers: usize,
65) -> Vec<AttackOutcome> {
66 let expanded = expand_all(attacks);
67
68 if workers <= 1 || expanded.len() <= 1 {
69 let interp = Interpreter::new(client, config.clone());
70 return interp.run(&expanded);
71 }
72
73 let worker_count = workers.min(expanded.len());
74 let chunks = split_indexed(&expanded, worker_count);
75
76 let mut indexed: Vec<(usize, AttackOutcome)> = thread::scope(|scope| {
77 let handles: Vec<_> = chunks
78 .into_iter()
79 .map(|chunk| {
80 scope.spawn(move || {
81 let interp = Interpreter::new(client, config.clone());
82 chunk
83 .into_iter()
84 .map(|(idx, attack)| {
85 (idx, interp.run(std::slice::from_ref(attack)).remove(0))
86 })
87 .collect::<Vec<_>>()
88 })
89 })
90 .collect();
91
92 handles
93 .into_iter()
94 .flat_map(|h| h.join().expect("worker thread panicked"))
95 .collect()
96 });
97
98 indexed.sort_by_key(|(i, _)| *i);
99 indexed.into_iter().map(|(_, o)| o).collect()
100}
101
102fn split_indexed(items: &[Attack], n: usize) -> Vec<Vec<(usize, &Attack)>> {
104 let n = n.max(1);
105 let mut chunks: Vec<Vec<(usize, &Attack)>> = vec![Vec::new(); n];
106 for (i, item) in items.iter().enumerate() {
107 chunks[i % n].push((i, item));
108 }
109 chunks
110}
111
112fn check_expectations(name: &str) -> Vec<Expectation> {
114 match name.to_ascii_lowercase().as_str() {
115 "authentication" | "auth" => vec![Expectation::Named {
116 name: "requires_auth".to_string(),
117 expected: true,
118 }],
119 "rate_limit" | "rate_limiting" | "ratelimit" => vec![Expectation::BlockedAfter(50)],
120 "injection" | "sql_injection" | "sqli" => vec![Expectation::Named {
121 name: "no_sql_error".to_string(),
122 expected: true,
123 }],
124 other => vec![Expectation::Named {
127 name: other.to_string(),
128 expected: true,
129 }],
130 }
131}
132
133fn set_send_field(send: &mut Vec<(String, Value)>, field: &str, value: &str) {
135 let v = Value::Str(value.to_string());
136 if let Some(entry) = send.iter_mut().find(|(k, _)| k == field) {
137 entry.1 = v;
138 } else {
139 send.push((field.to_string(), v));
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::klr::parser::parse;
147
148 #[test]
149 fn mutation_expands_into_variants() {
150 let src = r#"
151attack duplicate_payment {
152 request POST "/payment"
153 send { amount = 100 }
154 mutate amount {
155 negative_numbers
156 huge_values
157 decimals
158 }
159}
160"#;
161 let program = parse(src).unwrap();
162 let expanded = expand_all(&program.attacks);
163 assert_eq!(expanded.len(), 6);
165 for v in &expanded {
167 let amount = v.send.iter().find(|(k, _)| k == "amount").unwrap();
168 assert!(matches!(amount.1, Value::Str(_)));
169 assert!(v.name.contains("amount="));
170 }
171 }
172
173 #[test]
174 fn check_becomes_expectation() {
175 let src = r#"
176test login_security {
177 endpoint "/login"
178 check authentication
179}
180"#;
181 let program = parse(src).unwrap();
182 let expanded = expand_all(&program.attacks);
183 assert_eq!(expanded.len(), 1);
184 assert!(expanded[0].checks.is_empty());
185 assert_eq!(
186 expanded[0].expectations,
187 vec![Expectation::Named {
188 name: "requires_auth".to_string(),
189 expected: true
190 }]
191 );
192 }
193
194 #[test]
195 fn repeat_block_multiplies_request_count() {
196 let src = r#"
197repeat 100 {
198 attack login {
199 target "/login"
200 }
201}
202"#;
203 let program = parse(src).unwrap();
204 assert_eq!(program.attacks.len(), 1);
205 assert_eq!(program.attacks[0].repeat, Some(100));
206 }
207
208 #[test]
209 fn unknown_check_expands_to_a_named_expectation() {
210 let src = r#"
214test csrf_protection {
215 endpoint "/transfer"
216 check csrf
217}
218"#;
219 let program = parse(src).unwrap();
220 let expanded = expand_all(&program.attacks);
221 assert_eq!(
222 expanded[0].expectations,
223 vec![Expectation::Named {
224 name: "csrf".to_string(),
225 expected: true
226 }]
227 );
228 }
229
230 #[test]
231 fn split_covers_all_items_once() {
232 let attacks: Vec<Attack> = (0..10).map(|i| Attack::empty(format!("a{i}"), 1)).collect();
234 let chunks = split_indexed(&attacks, 3);
235 let mut seen: Vec<usize> = chunks
236 .iter()
237 .flat_map(|c| c.iter().map(|(i, _)| *i))
238 .collect();
239 seen.sort();
240 assert_eq!(seen, (0..10).collect::<Vec<_>>());
241 }
242}