Skip to main content

ferrox_guards/
squeezer.rs

1//! # Adversarial Feature Squeezer (`ferrox-guards::squeezer`)
2//!
3//! Implements payload canonicalization and feature squeezing to defend against
4//! adversarial evasion attacks (e.g., zero-width Unicode injection, URL double-encoding,
5//! and whitespace obfuscation).
6//!
7//! Literature reference: *ML Under Malware Attack* (Vorobeychik & Kantarcioglu).
8
9use serde::{Deserialize, Serialize};
10
11/// Result of an adversarial evasion check
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SqueezerAssessment {
14    pub is_evasion_detected: bool,
15    pub original_length: usize,
16    pub squeezed_length: usize,
17    pub zero_width_chars_removed: usize,
18    pub squeezed_payload: String,
19    pub rationale: String,
20}
21
22/// Feature Squeezer for input canonicalization and defense
23#[derive(Debug, Clone, Default)]
24pub struct FeatureSqueezer;
25
26impl FeatureSqueezer {
27    pub fn new() -> Self {
28        Self
29    }
30
31    /// Squeezes and canonicalizes a payload string to eliminate adversarial noise
32    pub fn squeeze_payload(&self, input: &str) -> SqueezerAssessment {
33        let original_len = input.len();
34        let mut zero_width_count = 0;
35        let mut cleaned = String::with_capacity(original_len);
36
37        // 1. Strip zero-width Unicode characters and control codes
38        for ch in input.chars() {
39            match ch {
40                '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{200E}' | '\u{200F}' | '\u{FEFF}' | '\u{00AD}' => {
41                    zero_width_count += 1;
42                }
43                c if c.is_control() && c != '\n' && c != '\r' && c != '\t' => {
44                    zero_width_count += 1;
45                }
46                c => {
47                    cleaned.push(c);
48                }
49            }
50        }
51
52        // 2. Canonicalize URL percentage encodings
53        let unencoded = Self::decode_url_percent(&cleaned);
54
55        // 3. Compress consecutive whitespace
56        let squeezed = Self::compress_whitespace(&unencoded);
57
58        let is_evasion = zero_width_count > 0 || (original_len > 0 && (squeezed.len() as f64 / original_len as f64) < 0.6);
59
60        let rationale = if zero_width_count > 0 {
61            format!("Removed {} zero-width/control characters used in evasion attempt", zero_width_count)
62        } else if is_evasion {
63            "Extreme payload compression ratio detected".to_string()
64        } else {
65            "Payload clean and canonicalized".to_string()
66        };
67
68        SqueezerAssessment {
69            is_evasion_detected: is_evasion,
70            original_length: original_len,
71            squeezed_length: squeezed.len(),
72            zero_width_chars_removed: zero_width_count,
73            squeezed_payload: squeezed,
74            rationale,
75        }
76    }
77
78    fn compress_whitespace(s: &str) -> String {
79        let mut result = String::with_capacity(s.len());
80        let mut in_space = false;
81        for c in s.chars() {
82            if c.is_whitespace() {
83                if !in_space {
84                    result.push(' ');
85                    in_space = true;
86                }
87            } else {
88                result.push(c);
89                in_space = false;
90            }
91        }
92        result.trim().to_string()
93    }
94
95    fn decode_url_percent(s: &str) -> String {
96        let mut out = String::with_capacity(s.len());
97        let bytes = s.as_bytes();
98        let mut i = 0;
99        while i < bytes.len() {
100            if bytes[i] == b'%' && i + 2 < bytes.len() {
101                if let (Some(h1), Some(h2)) = (Self::hex_val(bytes[i + 1]), Self::hex_val(bytes[i + 2])) {
102                    let decoded_byte = (h1 << 4) | h2;
103                    out.push(decoded_byte as char);
104                    i += 3;
105                    continue;
106                }
107            }
108            out.push(bytes[i] as char);
109            i += 1;
110        }
111        out
112    }
113
114    fn hex_val(b: u8) -> Option<u8> {
115        match b {
116            b'0'..=b'9' => Some(b - b'0'),
117            b'a'..=b'f' => Some(b - b'a' + 10),
118            b'A'..=b'F' => Some(b - b'A' + 10),
119            _ => None,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_squeezer_zero_width_evasion() {
130        let squeezer = FeatureSqueezer::new();
131        // Insert zero-width space in "SELECT"
132        let malicious_input = "S\u{200B}E\u{200B}L\u{200B}E\u{200B}C\u{200B}T * FROM users";
133        let res = squeezer.squeeze_payload(malicious_input);
134
135        assert!(res.is_evasion_detected);
136        assert_eq!(res.zero_width_chars_removed, 5);
137        assert_eq!(res.squeezed_payload, "SELECT * FROM users");
138    }
139
140    #[test]
141    fn test_squeezer_percent_encoding_canonicalization() {
142        let squeezer = FeatureSqueezer::new();
143        let input = "UNION%20SELECT%201,%202";
144        let res = squeezer.squeeze_payload(input);
145
146        assert_eq!(res.squeezed_payload, "UNION SELECT 1, 2");
147    }
148}