use parking_lot::RwLock;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{Engine, Request, RiskConfig, TraceSink, Verdict, WafError};
use crate::profiler::{EndpointProfile, ProfileStore, ProfileStoreConfig};
pub struct Synapse {
engine: Engine,
risk_config: RwLock<RiskConfig>,
profile_store: ProfileStore,
}
impl Default for Synapse {
fn default() -> Self {
Self::new()
}
}
impl Synapse {
pub fn new() -> Self {
Self {
engine: Engine::empty(),
risk_config: RwLock::new(RiskConfig::default()),
profile_store: ProfileStore::new(ProfileStoreConfig::default()),
}
}
pub fn with_profile_config(profile_config: ProfileStoreConfig) -> Self {
Self {
engine: Engine::empty(),
risk_config: RwLock::new(RiskConfig::default()),
profile_store: ProfileStore::new(profile_config),
}
}
pub fn load_rules(&mut self, json: &[u8]) -> Result<usize, WafError> {
self.engine.load_rules(json)
}
pub fn precompute_rules(
&self,
json: &[u8],
) -> Result<crate::waf::engine::CompiledRules, WafError> {
self.engine.precompute_rules(json)
}
pub fn reload_from_compiled(&mut self, compiled: crate::waf::engine::CompiledRules) {
self.engine.reload_from_compiled(compiled);
}
pub fn parse_rules(json: &[u8]) -> Result<Vec<crate::waf::WafRule>, WafError> {
Engine::parse_rules(json)
}
pub fn reload_rules(&mut self, rules: Vec<crate::waf::WafRule>) -> Result<(), WafError> {
self.engine.reload_rules(rules)
}
pub fn analyze(&self, req: &Request) -> Verdict {
self.engine.analyze(req)
}
pub fn analyze_with_trace(&self, req: &Request, trace: &mut dyn TraceSink) -> Verdict {
self.engine.analyze_with_trace(req, trace)
}
pub fn analyze_with_timeout(&self, req: &Request, timeout: std::time::Duration) -> Verdict {
self.engine.analyze_with_timeout(req, timeout)
}
pub fn analyze_safe(&self, req: &Request) -> Verdict {
self.engine.analyze_safe(req)
}
pub fn record_response_status(&self, path: &str, status: u16) {
let now_ms = Self::now_ms();
let mut profile = self.profile_store.get_or_create(path);
profile.update_response(0, status, None, now_ms);
}
pub fn get_profiles(&self) -> Vec<EndpointProfile> {
self.profile_store.get_profiles()
}
pub fn load_profiles(&self, profiles: Vec<EndpointProfile>) {
for profile in profiles {
let template = profile.template.clone();
let mut entry = self.profile_store.get_or_create(&template);
*entry = profile;
}
}
pub fn rule_count(&self) -> usize {
self.engine.rule_count()
}
pub fn risk_config(&self) -> RiskConfig {
self.risk_config.read().clone()
}
pub fn set_risk_config(&self, config: RiskConfig) {
*self.risk_config.write() = config;
}
pub fn profile_count(&self) -> usize {
self.profile_store.len()
}
pub fn clear_profiles(&self) {
self.profile_store.clear();
}
#[inline]
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_synapse() {
let synapse = Synapse::new();
assert_eq!(synapse.rule_count(), 0);
}
#[test]
fn test_load_rules() {
let mut synapse = Synapse::new();
let rules = r#"[
{
"id": 1,
"description": "SQL injection",
"risk": 10.0,
"blocking": true,
"matches": [
{"type": "uri", "match": {"type": "contains", "match": "' OR '"}}
]
}
]"#;
let count = synapse.load_rules(rules.as_bytes()).unwrap();
assert_eq!(count, 1);
assert_eq!(synapse.rule_count(), 1);
}
#[test]
fn test_default_synapse() {
let synapse = Synapse::default();
assert_eq!(synapse.rule_count(), 0);
}
#[test]
fn test_risk_config_get_set() {
use crate::waf::BlockingMode;
let synapse = Synapse::new();
let config = synapse.risk_config();
assert_eq!(config.max_risk, 100.0);
assert!(config.enable_repeat_multipliers);
let mut new_config = config.clone();
new_config.max_risk = 1000.0;
new_config.blocking_mode = BlockingMode::Enforcement;
new_config.anomaly_blocking_threshold = 25.0;
synapse.set_risk_config(new_config);
let updated = synapse.risk_config();
assert_eq!(updated.max_risk, 1000.0);
assert_eq!(updated.anomaly_blocking_threshold, 25.0);
assert!(matches!(updated.blocking_mode, BlockingMode::Enforcement));
}
#[test]
fn test_record_response_status() {
let synapse = Synapse::new();
assert_eq!(synapse.profile_count(), 0);
synapse.record_response_status("/api/users", 200);
synapse.record_response_status("/api/users", 200);
synapse.record_response_status("/api/users", 404);
assert_eq!(synapse.profile_count(), 1);
synapse.record_response_status("/api/orders", 200);
assert_eq!(synapse.profile_count(), 2);
}
#[test]
fn test_get_and_load_profiles() {
let synapse = Synapse::new();
synapse.record_response_status("/api/users", 200);
synapse.record_response_status("/api/orders", 200);
assert_eq!(synapse.profile_count(), 2);
let profiles = synapse.get_profiles();
assert_eq!(profiles.len(), 2);
synapse.clear_profiles();
assert_eq!(synapse.profile_count(), 0);
synapse.load_profiles(profiles);
assert_eq!(synapse.profile_count(), 2);
}
#[test]
fn test_profile_path_normalization() {
let synapse = Synapse::new();
synapse.record_response_status("/api/users/123", 200);
synapse.record_response_status("/api/users/456", 200);
let profiles = synapse.get_profiles();
assert!(!profiles.is_empty());
}
}