From 12841e8feefc54a1213d9cac3ecd7b86ce17e7e6 Mon Sep 17 00:00:00 2001 From: Raffael Schneider Date: Sat, 22 Aug 2026 11:40:11 +0200 Subject: [PATCH] deps: bump zentinel-modsec to 0.2.0 0.2.0 changes chain evaluation: a chain's actions now fire only when every link matches, and continuation rules inherit the starter's phase. That alters which requests are blocked, which is why the engine went to a minor version rather than a patch -- the upgrade should be deliberate. Adds tests/engine_behaviour.rs covering what operators actually depend on: complete chains block, partial chains do not, SQLi is detected, clean traffic passes, and anomaly scoring reaches its threshold. The agent's existing tests cover configuration and protocol, not blocking decisions, so an engine bump could previously change security behaviour with nothing here to catch it. --- Cargo.toml | 2 +- tests/engine_behaviour.rs | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/engine_behaviour.rs diff --git a/Cargo.toml b/Cargo.toml index cf3be02..78a4a3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ zentinel-common = "0.5" # Pure Rust ModSecurity implementation # >= 0.1.4 required: fixes the `&` count operator so stock OWASP CRS v4 evaluates # (earlier versions block every request). See zentinel-modsec#12. -zentinel-modsec = "0.1.4" +zentinel-modsec = "0.2.0" # Async runtime tokio = { version = "1.40", features = ["full"] } diff --git a/tests/engine_behaviour.rs b/tests/engine_behaviour.rs new file mode 100644 index 0000000..2553554 --- /dev/null +++ b/tests/engine_behaviour.rs @@ -0,0 +1,61 @@ +//! Behavioural checks against the underlying zentinel-modsec engine. +//! +//! The agent's own tests cover its configuration and protocol surface, but the +//! thing operators actually depend on is which requests get blocked. That is +//! decided by the engine, so a version bump there can change security +//! behaviour without touching a line of this crate. +//! +//! These ran green across the 0.1.4 -> 0.2.0 bump, which changed chain +//! evaluation (zentinel-modsec#18). + +use zentinel_modsec::ModSecurity; + +fn blocked(rules: &str, uri: &str, method: &str) -> bool { + let m = ModSecurity::from_string(rules).expect("rules should load"); + let mut tx = m.new_transaction(); + tx.process_uri(uri, method, "HTTP/1.1").unwrap(); + tx.add_request_header("Host", "example.com").unwrap(); + tx.process_request_headers().unwrap(); + tx.process_request_body().unwrap(); + tx.has_intervention() +} + +/// A chain is one logical rule: every link must match before it blocks. +#[test] +fn chained_rule_requires_every_link() { + let rules = "SecRuleEngine On\n\ + SecRule REQUEST_URI \"@contains admin\" \"id:1001,phase:1,deny,chain\"\n\ + SecRule REQUEST_METHOD \"@streq POST\""; + + assert!( + blocked(rules, "/admin", "POST"), + "complete chain should block" + ); + assert!( + !blocked(rules, "/admin", "GET"), + "partial chain match must not block — this is the false positive \ + zentinel-modsec#18 fixed" + ); +} + +#[test] +fn sqli_is_detected_and_clean_traffic_passes() { + let rules = "SecRuleEngine On\n\ + SecRule ARGS \"@detectSQLi\" \"id:2001,phase:2,deny\""; + + assert!(blocked(rules, "/?q=1' OR '1'='1--", "GET")); + assert!(!blocked(rules, "/?q=hello", "GET")); +} + +/// The anomaly-scoring path CRS is built around: macro-expanded thresholds +/// and `setvar` deltas both have to work for scoring to accumulate. +#[test] +fn anomaly_scoring_reaches_the_threshold() { + let rules = "SecRuleEngine On\n\ + SecAction \"id:3001,phase:1,pass,nolog,setvar:tx.threshold=5\"\n\ + SecRule ARGS \"@detectXSS\" \"id:3002,phase:2,pass,setvar:'tx.score=+5'\"\n\ + SecRule TX:score \"@ge %{tx.threshold}\" \"id:3003,phase:2,deny\""; + + assert!(blocked(rules, "/?q=", "GET")); + assert!(!blocked(rules, "/?q=hello", "GET")); +}