wafrift_evolution/
intelligence.rs1use crate::differential::{DifferentialResult, Probe, generate_probes, generate_quick_probes};
4use crate::evolution::{Chromosome, EvolutionEngine};
5use crate::types::{Budget, Feedback, LoopAction, OracleVerdict, TerminationReason};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9enum Phase {
10 DifferentialProbing,
11 Evolution,
12 Done,
13}
14
15#[derive(Debug, Clone)]
17pub struct IntelligenceLoop {
18 differential: DifferentialResult,
19 evolution: EvolutionEngine,
20 probes_completed: usize,
21 feedback_count: usize,
22 phase: Phase,
23 min_probes: usize,
24 probe_queue: Vec<Probe>,
25 eval_queue: Vec<(usize, Chromosome)>,
26 budget: Budget,
27}
28
29impl IntelligenceLoop {
30 #[must_use]
32 pub fn new(population_size: usize) -> Self {
33 Self::with_budget(population_size, 10, Budget::default())
34 }
35
36 #[must_use]
38 pub fn with_budget(population_size: usize, min_probes: usize, budget: Budget) -> Self {
39 let mut evolution = EvolutionEngine::new(population_size);
40 evolution.budget = budget;
41 Self {
42 differential: DifferentialResult::new(),
43 evolution,
44 probes_completed: 0,
45 feedback_count: 0,
46 phase: Phase::DifferentialProbing,
47 min_probes,
48 probe_queue: generate_probes(),
49 eval_queue: Vec::new(),
50 budget,
51 }
52 }
53
54 #[must_use]
56 pub fn generate_probes(&self) -> Vec<Probe> {
57 if self.probe_queue.len()
58 > self
59 .budget
60 .max_requests
61 .saturating_sub(self.probes_completed)
62 {
63 generate_quick_probes()
64 } else {
65 generate_probes()
66 }
67 }
68
69 #[must_use]
71 pub fn generate_quick_probes(&self) -> Vec<Probe> {
72 generate_quick_probes()
73 }
74
75 pub fn record_probe(&mut self, probe: &Probe, was_blocked: bool) {
77 self.differential.record(probe, was_blocked);
78 self.probes_completed += 1;
79 }
80
81 #[must_use]
83 pub fn differential_results(&self) -> &DifferentialResult {
84 &self.differential
85 }
86
87 #[must_use]
89 pub fn suggested_evasions(&self) -> Vec<String> {
90 self.differential.suggest_evasions()
91 }
92
93 #[must_use]
95 pub fn waf_report(&self) -> String {
96 self.differential.report()
97 }
98
99 #[must_use]
101 pub fn next_candidate(&mut self) -> Option<(usize, &Chromosome)> {
102 self.evolution.next_candidate()
103 }
104
105 pub fn batch_candidates(&mut self, n: usize) -> Vec<(usize, Chromosome)> {
107 self.evolution.batch_candidates(n)
108 }
109
110 pub fn record_feedback(&mut self, chromosome_index: usize, passed: bool) {
112 let _ = self.evolution.record_feedback(chromosome_index, passed);
113 self.feedback_count += 1;
114 }
115
116 pub fn record_verdict(&mut self, chromosome_index: usize, verdict: &OracleVerdict) {
118 let _ = self.evolution.record_verdict(chromosome_index, verdict);
119 self.feedback_count += 1;
120 }
121
122 pub fn evolve(&mut self) {
124 self.evolution.evolve();
125 }
126
127 #[must_use]
129 pub fn best_combination(&self) -> Option<&Chromosome> {
130 self.evolution.best()
131 }
132
133 #[must_use]
135 pub fn probes_completed(&self) -> usize {
136 self.probes_completed
137 }
138
139 #[must_use]
141 pub fn feedback_count(&self) -> usize {
142 self.feedback_count
143 }
144
145 #[must_use]
147 pub fn diversity(&self) -> f64 {
148 self.evolution.diversity_score()
149 }
150
151 #[must_use]
153 pub fn has_sufficient_data(&self) -> bool {
154 self.probes_completed >= self.min_probes
155 }
156
157 pub fn step(&mut self, feedback: Feedback) -> LoopAction {
163 if self.evolution.should_terminate() {
164 return LoopAction::Terminate(TerminationReason::BudgetExhausted);
165 }
166
167 if let Feedback::TargetError(ref msg) = feedback {
169 let _ = self.evolution.record_target_error(msg.clone());
170 if !self.evolution.target_health.is_healthy() {
171 return LoopAction::Terminate(TerminationReason::TargetHealthCritical);
172 }
173 }
175
176 match self.phase {
177 Phase::DifferentialProbing => {
178 if let Feedback::Blocked | Feedback::Passed = feedback {
179 }
181 if self.probe_queue.is_empty() || self.probes_completed >= self.min_probes {
182 self.phase = Phase::Evolution;
183 return self.step(Feedback::Passed); }
185 let probe = self.probe_queue.remove(0);
186 LoopAction::SendProbe(probe)
187 }
188 Phase::Evolution => {
189 if self.eval_queue.is_empty() {
190 let remaining = self
191 .budget
192 .max_requests
193 .saturating_sub(self.evolution.request_count);
194 let batch_size = 4_usize.min(remaining).max(1);
195 self.eval_queue = self.evolution.batch_candidates(batch_size);
196 if self.eval_queue.is_empty() {
197 self.phase = Phase::Done;
198 return LoopAction::Terminate(TerminationReason::BudgetExhausted);
199 }
200 }
201 let (_idx, chrom) = self.eval_queue.remove(0);
202 LoopAction::SendPayload(chrom)
203 }
204 Phase::Done => LoopAction::Terminate(TerminationReason::BudgetExhausted),
205 }
206 }
207
208 #[must_use]
210 pub fn suggested_delay_ms(&self) -> u64 {
211 if self.evolution.target_health.in_backoff() {
212 self.evolution.target_health.backoff().as_millis() as u64
213 } else {
214 0
215 }
216 }
217}
218
219impl Default for IntelligenceLoop {
220 fn default() -> Self {
221 Self::new(20)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn new_loop_default() {
231 let il = IntelligenceLoop::default();
232 assert_eq!(il.probes_completed(), 0);
233 assert_eq!(il.feedback_count(), 0);
234 assert!(!il.has_sufficient_data());
235 }
236
237 #[test]
238 fn generate_probes_not_empty() {
239 let il = IntelligenceLoop::default();
240 let probes = il.generate_probes();
241 assert!(!probes.is_empty());
242 }
243
244 #[test]
245 fn generate_quick_probes_smaller() {
246 let _il = IntelligenceLoop::default();
247 let full = generate_probes();
248 let quick = generate_quick_probes();
249 assert!(quick.len() < full.len());
250 }
251
252 #[test]
253 fn record_probe_increments() {
254 let mut il = IntelligenceLoop::default();
255 let probes = il.generate_quick_probes();
256 il.record_probe(&probes[0], true);
257 assert_eq!(il.probes_completed(), 1);
258 }
259
260 #[test]
261 fn sufficient_data_after_min_probes() {
262 let mut il = IntelligenceLoop::with_budget(10, 5, Budget::default());
263 let probes = il.generate_probes();
264 for (i, probe) in probes.iter().enumerate() {
265 il.record_probe(probe, i % 3 == 0);
266 if i >= 4 {
267 break;
268 }
269 }
270 assert!(il.has_sufficient_data());
271 }
272
273 #[test]
274 fn evolution_feedback_loop() {
275 let mut il = IntelligenceLoop::new(10);
276 if let Some((idx, _)) = il.next_candidate() {
277 il.record_feedback(idx, true);
278 assert_eq!(il.feedback_count(), 1);
279 }
280 }
281
282 #[test]
283 fn evolve_doesnt_panic() {
284 let mut il = IntelligenceLoop::new(10);
285 for _ in 0..5 {
286 if let Some((idx, _)) = il.next_candidate() {
287 il.record_feedback(idx, true);
288 }
289 }
290 il.evolve();
291 assert!(il.next_candidate().is_some());
292 }
293
294 #[test]
295 fn waf_report_not_empty_after_probes() {
296 let mut il = IntelligenceLoop::default();
297 let probes = il.generate_quick_probes();
298 for probe in &probes {
299 il.record_probe(probe, true);
300 }
301 let report = il.waf_report();
302 assert!(!report.is_empty());
303 }
304
305 #[test]
306 fn suggested_evasions_from_differential() {
307 let mut il = IntelligenceLoop::default();
308 let probes = generate_probes();
309 for probe in &probes {
310 let is_sql = format!("{:?}", probe.tests).contains("Sql");
311 il.record_probe(probe, is_sql);
312 }
313 let suggestions = il.suggested_evasions();
314 assert!(!suggestions.is_empty());
315 }
316
317 #[test]
318 fn diversity_score_valid_range() {
319 let il = IntelligenceLoop::new(10);
320 let score = il.diversity();
321 assert!((0.0..=1.0).contains(&score));
322 }
323
324 #[test]
325 fn step_state_machine_transitions() {
326 let mut il = IntelligenceLoop::with_budget(10, 2, Budget::default());
327 let action = il.step(Feedback::Passed);
329 assert!(matches!(action, LoopAction::SendProbe(_)));
330
331 il.record_probe(&generate_probes()[0], true);
332 let action2 = il.step(Feedback::Blocked);
333 assert!(matches!(action2, LoopAction::SendProbe(_)));
334
335 il.record_probe(&generate_probes()[1], false);
336 let action3 = il.step(Feedback::Passed);
338 assert!(matches!(action3, LoopAction::SendPayload(_)));
339 }
340
341 #[test]
342 fn step_terminates_on_target_error() {
343 let mut il = IntelligenceLoop::with_budget(10, 0, Budget::default());
344 for _ in 0..10 {
346 if let LoopAction::SendPayload(_) = il.step(Feedback::Passed) {
347 let term = il.step(Feedback::TargetError("503".into()));
349 assert!(matches!(
351 term,
352 LoopAction::SendPayload(_) | LoopAction::Terminate(_)
353 ));
354 return;
355 }
356 }
357 }
358}