Skip to main content

wafrift_evolution/differential/
analysis.rs

1use super::probe::{Probe, ProbeTarget};
2
3/// Results from a differential analysis run.
4#[derive(Debug, Clone)]
5pub struct DifferentialResult {
6    /// Which SQL keywords trigger the WAF.
7    pub blocked_sql_keywords: Vec<String>,
8    /// Which SQL operators trigger the WAF.
9    pub blocked_sql_operators: Vec<String>,
10    /// Which SQL comment styles trigger the WAF.
11    pub blocked_sql_comments: Vec<String>,
12    /// Whether SQL string delimiters trigger the WAF.
13    pub blocks_sql_quotes: bool,
14    /// Which tautology patterns trigger the WAF.
15    pub blocked_tautologies: Vec<String>,
16    /// Which HTML tags trigger the WAF.
17    pub blocked_xss_tags: Vec<String>,
18    /// Which event handlers trigger the WAF.
19    pub blocked_xss_events: Vec<String>,
20    /// Which JS execution functions trigger the WAF.
21    pub blocked_xss_functions: Vec<String>,
22    /// Which command separators trigger the WAF.
23    pub blocked_cmd_separators: Vec<String>,
24    /// Which shell commands trigger the WAF.
25    pub blocked_cmd_commands: Vec<String>,
26    /// Which file paths trigger the WAF.
27    pub blocked_cmd_paths: Vec<String>,
28    /// Whether the benign baseline was blocked.
29    pub baseline_blocked: bool,
30    /// Total probes sent.
31    pub total_probes: usize,
32    /// Total probes blocked.
33    pub total_blocked: usize,
34}
35
36impl DifferentialResult {
37    /// Create an empty result.
38    #[must_use]
39    pub fn new() -> Self {
40        Self {
41            blocked_sql_keywords: Vec::new(),
42            blocked_sql_operators: Vec::new(),
43            blocked_sql_comments: Vec::new(),
44            blocks_sql_quotes: false,
45            blocked_tautologies: Vec::new(),
46            blocked_xss_tags: Vec::new(),
47            blocked_xss_events: Vec::new(),
48            blocked_xss_functions: Vec::new(),
49            blocked_cmd_separators: Vec::new(),
50            blocked_cmd_commands: Vec::new(),
51            blocked_cmd_paths: Vec::new(),
52            baseline_blocked: false,
53            total_probes: 0,
54            total_blocked: 0,
55        }
56    }
57
58    /// Record a probe result.
59    pub fn record(&mut self, probe: &Probe, was_blocked: bool) {
60        self.total_probes += 1;
61        if was_blocked {
62            self.total_blocked += 1;
63        }
64        match &probe.tests {
65            ProbeTarget::Baseline => self.baseline_blocked = was_blocked,
66            ProbeTarget::SqlKeyword(keyword) => {
67                if was_blocked && !self.blocked_sql_keywords.contains(keyword) {
68                    self.blocked_sql_keywords.push(keyword.clone());
69                }
70            }
71            ProbeTarget::SqlOperator(operator) => {
72                if was_blocked && !self.blocked_sql_operators.contains(operator) {
73                    self.blocked_sql_operators.push(operator.clone());
74                }
75            }
76            ProbeTarget::SqlComment(comment) => {
77                if was_blocked && !self.blocked_sql_comments.contains(comment) {
78                    self.blocked_sql_comments.push(comment.clone());
79                }
80            }
81            ProbeTarget::SqlQuote => self.blocks_sql_quotes = was_blocked,
82            ProbeTarget::SqlTautology(tautology) => {
83                if was_blocked && !self.blocked_tautologies.contains(tautology) {
84                    self.blocked_tautologies.push(tautology.clone());
85                }
86            }
87            ProbeTarget::XssTag(tag) => {
88                if was_blocked && !self.blocked_xss_tags.contains(tag) {
89                    self.blocked_xss_tags.push(tag.clone());
90                }
91            }
92            ProbeTarget::XssEvent(event) => {
93                if was_blocked && !self.blocked_xss_events.contains(event) {
94                    self.blocked_xss_events.push(event.clone());
95                }
96            }
97            ProbeTarget::XssExecFunction(function) => {
98                if was_blocked && !self.blocked_xss_functions.contains(function) {
99                    self.blocked_xss_functions.push(function.clone());
100                }
101            }
102            ProbeTarget::CmdSeparator(separator) => {
103                if was_blocked && !self.blocked_cmd_separators.contains(separator) {
104                    self.blocked_cmd_separators.push(separator.clone());
105                }
106            }
107            ProbeTarget::CmdCommand(command) => {
108                if was_blocked && !self.blocked_cmd_commands.contains(command) {
109                    self.blocked_cmd_commands.push(command.clone());
110                }
111            }
112            ProbeTarget::CmdPath(path) => {
113                if was_blocked && !self.blocked_cmd_paths.contains(path) {
114                    self.blocked_cmd_paths.push(path.clone());
115                }
116            }
117        }
118    }
119}
120impl Default for DifferentialResult {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::DifferentialResult;
129    use crate::differential::{ProbeTarget, generate_quick_probes};
130
131    #[test]
132    fn record_basic_results() {
133        let probes = generate_quick_probes();
134        let mut result = DifferentialResult::new();
135        for probe in &probes {
136            let blocked = match &probe.tests {
137                ProbeTarget::SqlTautology(_) | ProbeTarget::SqlKeyword(_) => true,
138                ProbeTarget::XssTag(tag) if tag == "script" => true,
139                _ => false,
140            };
141            result.record(probe, blocked);
142        }
143        assert_eq!(result.total_probes, probes.len());
144        assert!(result.total_blocked > 0);
145        assert!(!result.baseline_blocked);
146    }
147
148    #[test]
149    fn record_deduplicates() {
150        let mut result = DifferentialResult::new();
151        let probe = crate::differential::Probe {
152            payload: "test".into(),
153            tests: ProbeTarget::SqlKeyword("SELECT".into()),
154            description: "test".into(),
155            expected_blocked: true,
156        };
157        result.record(&probe, true);
158        result.record(&probe, true);
159        assert_eq!(result.blocked_sql_keywords.len(), 1);
160        assert_eq!(result.blocked_sql_keywords[0], "SELECT");
161    }
162
163    #[test]
164    fn default_impl_equivalent_to_new() {
165        let first = DifferentialResult::new();
166        let second = DifferentialResult::default();
167        assert_eq!(first.total_probes, second.total_probes);
168        assert_eq!(first.total_blocked, second.total_blocked);
169    }
170}