wafrift_evolution/differential/
report.rs1use super::analysis::DifferentialResult;
2
3impl DifferentialResult {
4 #[must_use]
6 pub fn report(&self) -> String {
7 let mut lines = vec![
8 "=== WAF Differential Analysis ===".to_string(),
9 format!("Probes sent: {}", self.total_probes),
10 format!(
11 "Probes blocked: {} ({:.0}%)",
12 self.total_blocked,
13 if self.total_probes > 0 {
14 self.total_blocked as f64 / self.total_probes as f64 * 100.0
15 } else {
16 0.0
17 }
18 ),
19 ];
20 if self.baseline_blocked {
21 lines.push(
22 "warning: baseline (benign) request was blocked; WAF may be over-aggressive".into(),
23 );
24 }
25 append_section(
26 &mut lines,
27 "\nSQL Keywords blocked",
28 &self.blocked_sql_keywords,
29 );
30 append_section(
31 &mut lines,
32 "SQL Operators blocked",
33 &self.blocked_sql_operators,
34 );
35 append_section(
36 &mut lines,
37 "SQL Comments blocked",
38 &self.blocked_sql_comments,
39 );
40 if self.blocks_sql_quotes {
41 lines.push("SQL Quotes blocked: YES (single quotes trigger the WAF)".into());
42 }
43 append_section(
44 &mut lines,
45 "SQL Tautologies blocked",
46 &self.blocked_tautologies,
47 );
48 append_section(&mut lines, "\nXSS Tags blocked", &self.blocked_xss_tags);
49 append_section(&mut lines, "XSS Events blocked", &self.blocked_xss_events);
50 append_section(
51 &mut lines,
52 "XSS Functions blocked",
53 &self.blocked_xss_functions,
54 );
55 append_section(
56 &mut lines,
57 "\nCMD Separators blocked",
58 &self.blocked_cmd_separators,
59 );
60 append_section(
61 &mut lines,
62 "CMD Commands blocked",
63 &self.blocked_cmd_commands,
64 );
65 append_section(&mut lines, "CMD Paths blocked", &self.blocked_cmd_paths);
66 lines.push("\n=== Gaps (what the WAF does NOT block) ===".into());
67 append_gap_section(
68 &mut lines,
69 "SQL Keywords NOT blocked",
70 &[
71 "SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "FROM", "WHERE",
72 "ORDER BY", "HAVING",
73 ],
74 &self.blocked_sql_keywords,
75 );
76 append_gap_section(
77 &mut lines,
78 "XSS Tags NOT blocked",
79 &[
80 "script", "img", "svg", "iframe", "body", "details", "input", "marquee", "video",
81 "object",
82 ],
83 &self.blocked_xss_tags,
84 );
85 lines.join("\n")
86 }
87
88 #[must_use]
90 pub fn suggest_evasions(&self) -> Vec<String> {
91 let mut suggestions = Vec::new();
92 if !self.blocked_sql_keywords.is_empty() && self.blocked_sql_comments.len() < 3 {
93 suggestions.push(
94 "SqlCommentInsertion — WAF blocks keywords but may not handle inline comments"
95 .into(),
96 );
97 }
98 if self.blocked_xss_tags.iter().any(|tag| tag == "script")
99 && !self.blocked_xss_tags.iter().any(|tag| tag == "details")
100 {
101 suggestions
102 .push("XSS via <details ontoggle> — script blocked but details tag not".into());
103 }
104 if self.blocked_xss_tags.iter().any(|tag| tag == "script")
105 && !self.blocked_xss_tags.iter().any(|tag| tag == "svg")
106 {
107 suggestions.push("XSS via <svg onload> — script blocked but SVG not".into());
108 }
109 if self
110 .blocked_xss_functions
111 .iter()
112 .any(|function| function.contains("alert"))
113 && !self
114 .blocked_xss_functions
115 .iter()
116 .any(|function| function.contains("constructor"))
117 {
118 suggestions.push(
119 "XSS via constructor chain — alert() blocked but prototype access not".into(),
120 );
121 }
122 if self
123 .blocked_cmd_separators
124 .iter()
125 .any(|separator| separator == ";")
126 && !self
127 .blocked_cmd_separators
128 .iter()
129 .any(|separator| separator == "|")
130 {
131 suggestions
132 .push("CMD injection via pipe (|) — semicolons blocked but pipes not".into());
133 }
134 if !self.blocked_cmd_commands.is_empty() {
135 suggestions.push(
136 "CMD obfuscation (backslash, quotes, hex encoding) — command names blocked".into(),
137 );
138 }
139 if self
140 .blocked_tautologies
141 .iter()
142 .any(|tautology| tautology == "1=1")
143 && !self
144 .blocked_tautologies
145 .iter()
146 .any(|tautology| tautology.contains("BETWEEN"))
147 {
148 suggestions.push("SQL tautology via BETWEEN — 1=1 blocked but BETWEEN not".into());
149 }
150 if suggestions.is_empty() {
151 suggestions.push(
152 "WAF appears comprehensive — try Content-Type switching or encoding layering"
153 .into(),
154 );
155 }
156 suggestions
157 }
158}
159
160fn append_section(lines: &mut Vec<String>, label: &str, blocked: &[String]) {
161 if !blocked.is_empty() {
162 lines.push(format!("{label}: {}", blocked.join(", ")));
163 }
164}
165
166fn append_gap_section(lines: &mut Vec<String>, label: &str, all: &[&str], blocked: &[String]) {
167 let unblocked: Vec<&str> = all
168 .iter()
169 .copied()
170 .filter(|candidate| {
171 !blocked
172 .iter()
173 .any(|existing| existing.eq_ignore_ascii_case(candidate))
174 })
175 .collect();
176 if !unblocked.is_empty() {
177 lines.push(format!("{label}: {unblocked:?}"));
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use crate::differential::DifferentialResult;
184
185 #[test]
186 fn report_includes_sections() {
187 let mut result = DifferentialResult::new();
188 result.blocked_sql_keywords.push("SELECT".into());
189 result.blocked_xss_tags.push("script".into());
190 result.total_probes = 10;
191 result.total_blocked = 2;
192 let report = result.report();
193 assert!(report.contains("SELECT"));
194 assert!(report.contains("script"));
195 assert!(report.contains("Probes sent: 10"));
196 }
197
198 #[test]
199 fn suggest_evasions_finds_gaps() {
200 let mut result = DifferentialResult::new();
201 result.blocked_sql_keywords.push("SELECT".into());
202 result.blocked_sql_comments.push("--".into());
203 assert!(!result.suggest_evasions().is_empty());
204 }
205
206 #[test]
207 fn suggest_evasions_xss_tag_gap() {
208 let mut result = DifferentialResult::new();
209 result.blocked_xss_tags.push("script".into());
210 let suggestions = result.suggest_evasions();
211 let has_svg = suggestions
212 .iter()
213 .any(|suggestion| suggestion.contains("svg"));
214 let has_details = suggestions
215 .iter()
216 .any(|suggestion| suggestion.contains("details"));
217 assert!(has_svg || has_details, "should suggest unblocked tags");
218 }
219
220 #[test]
221 fn suggest_evasions_cmd_separator_gap() {
222 let mut result = DifferentialResult::new();
223 result.blocked_cmd_separators.push(";".into());
224 let has_pipe = result
225 .suggest_evasions()
226 .iter()
227 .any(|suggestion| suggestion.contains("pipe") || suggestion.contains('|'));
228 assert!(has_pipe, "should suggest pipe when semicolons blocked");
229 }
230
231 #[test]
232 fn report_warns_on_baseline_blocked() {
233 let mut result = DifferentialResult::new();
234 result.baseline_blocked = true;
235 let report = result.report();
236 assert!(report.contains("warning"));
237 }
238}