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) {
114 if let Err(e) = self.evolution.record_feedback(chromosome_index, passed) {
115 tracing::warn!(?e, chromosome_index, "evolution.record_feedback rejected — likely stale chromosome index");
116 }
117 self.feedback_count += 1;
118 }
119
120 pub fn record_verdict(&mut self, chromosome_index: usize, verdict: &OracleVerdict) {
123 if let Err(e) = self.evolution.record_verdict(chromosome_index, verdict) {
124 tracing::warn!(?e, chromosome_index, "evolution.record_verdict rejected — likely stale chromosome index");
125 }
126 self.feedback_count += 1;
127 }
128
129 pub fn evolve(&mut self) {
131 self.evolution.evolve();
132 }
133
134 #[must_use]
136 pub fn best_combination(&self) -> Option<&Chromosome> {
137 self.evolution.best()
138 }
139
140 #[must_use]
142 pub fn probes_completed(&self) -> usize {
143 self.probes_completed
144 }
145
146 #[must_use]
148 pub fn feedback_count(&self) -> usize {
149 self.feedback_count
150 }
151
152 #[must_use]
154 pub fn diversity(&self) -> f64 {
155 self.evolution.diversity_score()
156 }
157
158 #[must_use]
160 pub fn has_sufficient_data(&self) -> bool {
161 self.probes_completed >= self.min_probes
162 }
163
164 pub fn step(&mut self, feedback: Feedback) -> LoopAction {
170 if self.evolution.should_terminate() {
171 return LoopAction::Terminate(TerminationReason::BudgetExhausted);
172 }
173
174 if let Feedback::TargetError(ref msg) = feedback {
176 let _ = self.evolution.record_target_error(msg.clone());
177 if !self.evolution.target_health.is_healthy() {
178 return LoopAction::Terminate(TerminationReason::TargetHealthCritical);
179 }
180 }
182
183 match self.phase {
184 Phase::DifferentialProbing => {
185 if let Feedback::Blocked | Feedback::Passed = feedback {
186 }
188 if self.probe_queue.is_empty() || self.probes_completed >= self.min_probes {
189 self.phase = Phase::Evolution;
190 return self.step(Feedback::Passed); }
192 let probe = self.probe_queue.remove(0);
193 LoopAction::SendProbe(probe)
194 }
195 Phase::Evolution => {
196 if self.eval_queue.is_empty() {
197 let remaining = self
198 .budget
199 .max_requests
200 .saturating_sub(self.evolution.request_count);
201 let batch_size = 4_usize.min(remaining).max(1);
202 self.eval_queue = self.evolution.batch_candidates(batch_size);
203 if self.eval_queue.is_empty() {
204 self.phase = Phase::Done;
205 return LoopAction::Terminate(TerminationReason::BudgetExhausted);
206 }
207 }
208 let (_idx, chrom) = self.eval_queue.remove(0);
209 LoopAction::SendPayload(chrom)
210 }
211 Phase::Done => LoopAction::Terminate(TerminationReason::BudgetExhausted),
212 }
213 }
214
215 #[must_use]
217 pub fn suggested_delay_ms(&self) -> u64 {
218 if self.evolution.target_health.in_backoff() {
219 self.evolution.target_health.backoff().as_millis() as u64
220 } else {
221 0
222 }
223 }
224}
225
226impl Default for IntelligenceLoop {
227 fn default() -> Self {
228 Self::new(20)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn new_loop_default() {
238 let il = IntelligenceLoop::default();
239 assert_eq!(il.probes_completed(), 0);
240 assert_eq!(il.feedback_count(), 0);
241 assert!(!il.has_sufficient_data());
242 }
243
244 #[test]
245 fn generate_probes_not_empty() {
246 let il = IntelligenceLoop::default();
247 let probes = il.generate_probes();
248 assert!(!probes.is_empty());
249 }
250
251 #[test]
252 fn generate_quick_probes_smaller() {
253 let _il = IntelligenceLoop::default();
254 let full = generate_probes();
255 let quick = generate_quick_probes();
256 assert!(quick.len() < full.len());
257 }
258
259 #[test]
260 fn record_probe_increments() {
261 let mut il = IntelligenceLoop::default();
262 let probes = il.generate_quick_probes();
263 il.record_probe(&probes[0], true);
264 assert_eq!(il.probes_completed(), 1);
265 }
266
267 #[test]
268 fn sufficient_data_after_min_probes() {
269 let mut il = IntelligenceLoop::with_budget(10, 5, Budget::default());
270 let probes = il.generate_probes();
271 for (i, probe) in probes.iter().enumerate() {
272 il.record_probe(probe, i % 3 == 0);
273 if i >= 4 {
274 break;
275 }
276 }
277 assert!(il.has_sufficient_data());
278 }
279
280 #[test]
281 fn evolution_feedback_loop() {
282 let mut il = IntelligenceLoop::new(10);
283 if let Some((idx, _)) = il.next_candidate() {
284 il.record_feedback(idx, true);
285 assert_eq!(il.feedback_count(), 1);
286 }
287 }
288
289 #[test]
290 fn evolve_doesnt_panic() {
291 let mut il = IntelligenceLoop::new(10);
292 for _ in 0..5 {
293 if let Some((idx, _)) = il.next_candidate() {
294 il.record_feedback(idx, true);
295 }
296 }
297 il.evolve();
298 assert!(il.next_candidate().is_some());
299 }
300
301 #[test]
302 fn waf_report_not_empty_after_probes() {
303 let mut il = IntelligenceLoop::default();
304 let probes = il.generate_quick_probes();
305 for probe in &probes {
306 il.record_probe(probe, true);
307 }
308 let report = il.waf_report();
309 assert!(!report.is_empty());
310 }
311
312 #[test]
313 fn suggested_evasions_from_differential() {
314 let mut il = IntelligenceLoop::default();
315 let probes = generate_probes();
316 for probe in &probes {
317 let is_sql = format!("{:?}", probe.tests).contains("Sql");
318 il.record_probe(probe, is_sql);
319 }
320 let suggestions = il.suggested_evasions();
321 assert!(!suggestions.is_empty());
322 }
323
324 #[test]
325 fn diversity_score_valid_range() {
326 let il = IntelligenceLoop::new(10);
327 let score = il.diversity();
328 assert!((0.0..=1.0).contains(&score));
329 }
330
331 #[test]
332 fn step_state_machine_transitions() {
333 let mut il = IntelligenceLoop::with_budget(10, 2, Budget::default());
334 let action = il.step(Feedback::Passed);
336 assert!(matches!(action, LoopAction::SendProbe(_)));
337
338 il.record_probe(&generate_probes()[0], true);
339 let action2 = il.step(Feedback::Blocked);
340 assert!(matches!(action2, LoopAction::SendProbe(_)));
341
342 il.record_probe(&generate_probes()[1], false);
343 let action3 = il.step(Feedback::Passed);
345 assert!(matches!(action3, LoopAction::SendPayload(_)));
346 }
347
348 #[test]
349 fn step_terminates_on_target_error() {
350 let mut il = IntelligenceLoop::with_budget(10, 0, Budget::default());
351 for _ in 0..10 {
353 if let LoopAction::SendPayload(_) = il.step(Feedback::Passed) {
354 let term = il.step(Feedback::TargetError("503".into()));
356 assert!(matches!(
358 term,
359 LoopAction::SendPayload(_) | LoopAction::Terminate(_)
360 ));
361 return;
362 }
363 }
364 }
365}