Skip to main content

synapse_pingora/waf/
rule.rs

1//! Rule definitions and deserialization.
2
3use serde::{Deserialize, Serialize};
4
5/// WAF rule definition.
6#[derive(Deserialize, Serialize, Clone, Debug)]
7pub struct WafRule {
8    pub id: u32,
9    #[allow(dead_code)]
10    pub description: String,
11    #[serde(default)]
12    pub contributing_score: Option<f64>,
13    #[serde(default)]
14    pub risk: Option<f64>,
15    #[serde(default)]
16    pub blocking: Option<bool>,
17    pub matches: Vec<MatchCondition>,
18}
19
20impl WafRule {
21    /// Get the effective risk score for this rule.
22    pub fn effective_risk(&self) -> f64 {
23        if let Some(r) = self.risk {
24            if r.is_finite() {
25                return r;
26            }
27        }
28        if let Some(r) = self.contributing_score {
29            if r.is_finite() {
30                return r;
31            }
32        }
33        0.0
34    }
35}
36
37/// Match condition for rule evaluation.
38#[derive(Deserialize, Serialize, Clone, Debug)]
39pub struct MatchCondition {
40    #[serde(rename = "type")]
41    pub kind: String,
42    #[serde(rename = "match", default)]
43    pub match_value: Option<MatchValue>,
44    #[serde(default)]
45    pub op: Option<String>,
46    #[serde(default)]
47    pub field: Option<String>,
48    #[serde(default)]
49    pub direction: Option<String>,
50    #[serde(default)]
51    #[allow(dead_code)]
52    pub field_type: Option<String>,
53    #[serde(default)]
54    pub name: Option<String>,
55    #[serde(default)]
56    pub selector: Option<Box<MatchCondition>>,
57    #[serde(default)]
58    #[allow(dead_code)]
59    pub cleanup_after: Option<u64>,
60    #[serde(default)]
61    pub count: Option<u64>,
62    #[serde(default)]
63    pub timeframe: Option<u64>,
64}
65
66/// Match value variants.
67#[derive(Deserialize, Serialize, Clone, Debug)]
68#[serde(untagged)]
69pub enum MatchValue {
70    Str(String),
71    Num(f64),
72    Bool(bool),
73    Arr(Vec<MatchValue>),
74    Cond(Box<MatchCondition>),
75    #[allow(dead_code)]
76    Json(serde_json::Value),
77}
78
79impl MatchValue {
80    pub fn as_str(&self) -> Option<&str> {
81        match self {
82            MatchValue::Str(s) => Some(s.as_str()),
83            _ => None,
84        }
85    }
86
87    pub fn as_num(&self) -> Option<f64> {
88        match self {
89            MatchValue::Num(n) => Some(*n),
90            MatchValue::Str(s) => s.parse::<f64>().ok(),
91            _ => None,
92        }
93    }
94
95    pub fn as_bool(&self) -> Option<bool> {
96        match self {
97            MatchValue::Bool(b) => Some(*b),
98            _ => None,
99        }
100    }
101
102    pub fn as_arr(&self) -> Option<&[MatchValue]> {
103        match self {
104            MatchValue::Arr(items) => Some(items.as_slice()),
105            _ => None,
106        }
107    }
108
109    pub fn as_cond(&self) -> Option<&MatchCondition> {
110        match self {
111            MatchValue::Cond(c) => Some(c.as_ref()),
112            _ => None,
113        }
114    }
115}
116
117/// Get boolean operands from a condition.
118pub fn boolean_operands(condition: &MatchCondition) -> Vec<&MatchCondition> {
119    let Some(match_value) = condition.match_value.as_ref() else {
120        return Vec::new();
121    };
122    if let Some(items) = match_value.as_arr() {
123        return items.iter().filter_map(|v| v.as_cond()).collect();
124    }
125    match_value.as_cond().map(|c| vec![c]).unwrap_or_default()
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_parse_simple_rule() {
134        let json = r#"{
135            "id": 1,
136            "description": "Test rule",
137            "risk": 10.0,
138            "blocking": true,
139            "matches": [
140                {"type": "method", "match": "GET"}
141            ]
142        }"#;
143
144        let rule: WafRule = serde_json::from_str(json).unwrap();
145        assert_eq!(rule.id, 1);
146        assert_eq!(rule.effective_risk(), 10.0);
147        assert_eq!(rule.blocking, Some(true));
148        assert_eq!(rule.matches.len(), 1);
149    }
150
151    #[test]
152    fn test_parse_nested_condition() {
153        let json = r#"{
154            "id": 2,
155            "description": "Nested rule",
156            "matches": [
157                {
158                    "type": "uri",
159                    "match": {
160                        "type": "contains",
161                        "match": "admin"
162                    }
163                }
164            ]
165        }"#;
166
167        let rule: WafRule = serde_json::from_str(json).unwrap();
168        assert_eq!(rule.matches[0].kind, "uri");
169        let inner = rule.matches[0]
170            .match_value
171            .as_ref()
172            .unwrap()
173            .as_cond()
174            .unwrap();
175        assert_eq!(inner.kind, "contains");
176    }
177}