Skip to main content

wafrift_evolution/differential/
report.rs

1use super::analysis::DifferentialResult;
2
3impl DifferentialResult {
4    /// Generate a human-readable report of what the WAF blocks.
5    #[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    /// Suggest evasion strategies based on observed WAF behavior.
89    #[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}