synapse_pingora/waf/
rule.rs1use serde::{Deserialize, Serialize};
4
5#[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 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#[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#[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
117pub 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}