synapse_pingora/waf/
synapse.rs1use parking_lot::RwLock;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use super::{Engine, Request, RiskConfig, TraceSink, Verdict, WafError};
10use crate::profiler::{EndpointProfile, ProfileStore, ProfileStoreConfig};
11
12pub struct Synapse {
34 engine: Engine,
35 risk_config: RwLock<RiskConfig>,
37 profile_store: ProfileStore,
39}
40
41impl Default for Synapse {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl Synapse {
48 pub fn new() -> Self {
50 Self {
51 engine: Engine::empty(),
52 risk_config: RwLock::new(RiskConfig::default()),
53 profile_store: ProfileStore::new(ProfileStoreConfig::default()),
54 }
55 }
56
57 pub fn with_profile_config(profile_config: ProfileStoreConfig) -> Self {
59 Self {
60 engine: Engine::empty(),
61 risk_config: RwLock::new(RiskConfig::default()),
62 profile_store: ProfileStore::new(profile_config),
63 }
64 }
65
66 pub fn load_rules(&mut self, json: &[u8]) -> Result<usize, WafError> {
70 self.engine.load_rules(json)
71 }
72
73 pub fn precompute_rules(
75 &self,
76 json: &[u8],
77 ) -> Result<crate::waf::engine::CompiledRules, WafError> {
78 self.engine.precompute_rules(json)
79 }
80
81 pub fn reload_from_compiled(&mut self, compiled: crate::waf::engine::CompiledRules) {
83 self.engine.reload_from_compiled(compiled);
84 }
85
86 pub fn parse_rules(json: &[u8]) -> Result<Vec<crate::waf::WafRule>, WafError> {
88 Engine::parse_rules(json)
89 }
90
91 pub fn reload_rules(&mut self, rules: Vec<crate::waf::WafRule>) -> Result<(), WafError> {
93 self.engine.reload_rules(rules)
94 }
95
96 pub fn analyze(&self, req: &Request) -> Verdict {
98 self.engine.analyze(req)
99 }
100
101 pub fn analyze_with_trace(&self, req: &Request, trace: &mut dyn TraceSink) -> Verdict {
103 self.engine.analyze_with_trace(req, trace)
104 }
105
106 pub fn analyze_with_timeout(&self, req: &Request, timeout: std::time::Duration) -> Verdict {
115 self.engine.analyze_with_timeout(req, timeout)
116 }
117
118 pub fn analyze_safe(&self, req: &Request) -> Verdict {
122 self.engine.analyze_safe(req)
123 }
124
125 pub fn record_response_status(&self, path: &str, status: u16) {
130 let now_ms = Self::now_ms();
131 let mut profile = self.profile_store.get_or_create(path);
132 profile.update_response(0, status, None, now_ms);
135 }
136
137 pub fn get_profiles(&self) -> Vec<EndpointProfile> {
141 self.profile_store.get_profiles()
142 }
143
144 pub fn load_profiles(&self, profiles: Vec<EndpointProfile>) {
148 for profile in profiles {
149 let template = profile.template.clone();
151 let mut entry = self.profile_store.get_or_create(&template);
152 *entry = profile;
154 }
155 }
156
157 pub fn rule_count(&self) -> usize {
159 self.engine.rule_count()
160 }
161
162 pub fn risk_config(&self) -> RiskConfig {
164 self.risk_config.read().clone()
165 }
166
167 pub fn set_risk_config(&self, config: RiskConfig) {
171 *self.risk_config.write() = config;
172 }
173
174 pub fn profile_count(&self) -> usize {
176 self.profile_store.len()
177 }
178
179 pub fn clear_profiles(&self) {
181 self.profile_store.clear();
182 }
183
184 #[inline]
186 fn now_ms() -> u64 {
187 SystemTime::now()
188 .duration_since(UNIX_EPOCH)
189 .map(|d| d.as_millis() as u64)
190 .unwrap_or(0)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_new_synapse() {
200 let synapse = Synapse::new();
201 assert_eq!(synapse.rule_count(), 0);
202 }
203
204 #[test]
205 fn test_load_rules() {
206 let mut synapse = Synapse::new();
207 let rules = r#"[
208 {
209 "id": 1,
210 "description": "SQL injection",
211 "risk": 10.0,
212 "blocking": true,
213 "matches": [
214 {"type": "uri", "match": {"type": "contains", "match": "' OR '"}}
215 ]
216 }
217 ]"#;
218 let count = synapse.load_rules(rules.as_bytes()).unwrap();
219 assert_eq!(count, 1);
220 assert_eq!(synapse.rule_count(), 1);
221 }
222
223 #[test]
224 fn test_default_synapse() {
225 let synapse = Synapse::default();
226 assert_eq!(synapse.rule_count(), 0);
227 }
228
229 #[test]
230 fn test_risk_config_get_set() {
231 use crate::waf::BlockingMode;
232
233 let synapse = Synapse::new();
234
235 let config = synapse.risk_config();
237 assert_eq!(config.max_risk, 100.0);
238 assert!(config.enable_repeat_multipliers);
239
240 let mut new_config = config.clone();
242 new_config.max_risk = 1000.0;
243 new_config.blocking_mode = BlockingMode::Enforcement;
244 new_config.anomaly_blocking_threshold = 25.0;
245 synapse.set_risk_config(new_config);
246
247 let updated = synapse.risk_config();
249 assert_eq!(updated.max_risk, 1000.0);
250 assert_eq!(updated.anomaly_blocking_threshold, 25.0);
251 assert!(matches!(updated.blocking_mode, BlockingMode::Enforcement));
252 }
253
254 #[test]
255 fn test_record_response_status() {
256 let synapse = Synapse::new();
257
258 assert_eq!(synapse.profile_count(), 0);
260
261 synapse.record_response_status("/api/users", 200);
263 synapse.record_response_status("/api/users", 200);
264 synapse.record_response_status("/api/users", 404);
265
266 assert_eq!(synapse.profile_count(), 1);
268
269 synapse.record_response_status("/api/orders", 200);
271 assert_eq!(synapse.profile_count(), 2);
272 }
273
274 #[test]
275 fn test_get_and_load_profiles() {
276 let synapse = Synapse::new();
277
278 synapse.record_response_status("/api/users", 200);
280 synapse.record_response_status("/api/orders", 200);
281 assert_eq!(synapse.profile_count(), 2);
282
283 let profiles = synapse.get_profiles();
285 assert_eq!(profiles.len(), 2);
286
287 synapse.clear_profiles();
289 assert_eq!(synapse.profile_count(), 0);
290
291 synapse.load_profiles(profiles);
293 assert_eq!(synapse.profile_count(), 2);
294 }
295
296 #[test]
297 fn test_profile_path_normalization() {
298 let synapse = Synapse::new();
299
300 synapse.record_response_status("/api/users/123", 200);
302 synapse.record_response_status("/api/users/456", 200);
303
304 let profiles = synapse.get_profiles();
307 assert!(!profiles.is_empty());
308 }
309}