From 1668d789493f5f6b19b856252f29e5de9462fa12 Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 10:07:08 +0200 Subject: [PATCH] fix(config): pass excluded_semantic_detection_headers and detection_max_scan_seconds through from_security_config Both detection fields were dropped by from_security_config. They now reach the engine through DetectionExclusionConfig, with tests that show each value installed and a spent scan time budget blocking. --- src/fairing.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/fairing.rs b/src/fairing.rs index 18c8afc..5c9b19e 100644 --- a/src/fairing.rs +++ b/src/fairing.rs @@ -350,9 +350,14 @@ impl GuardFairing { // field; clippy calls it needless while the field list is complete. #[allow(clippy::needless_update)] if !config.excluded_detection_headers.is_empty() + || !config.excluded_semantic_detection_headers.is_empty() || !config.excluded_detection_params.is_empty() || !config.excluded_detection_body_fields.is_empty() || !config.enabled_detection_categories.is_empty() + || (config.detection_max_scan_seconds + - guard_core_engine::scan_budget::DEFAULT_MAX_SCAN_SECONDS) + .abs() + > f64::EPSILON { fairing = fairing.with_detection_exclusions(DetectionExclusionConfig { excluded_detection_headers: config @@ -360,6 +365,11 @@ impl GuardFairing { .iter() .cloned() .collect(), + excluded_semantic_detection_headers: config + .excluded_semantic_detection_headers + .iter() + .cloned() + .collect(), excluded_detection_params: config .excluded_detection_params .iter() @@ -379,6 +389,7 @@ impl GuardFairing { .collect() }), detection_scan_body: Some(config.detection_scan_body), + max_scan_seconds: Some(config.detection_max_scan_seconds), ..DetectionExclusionConfig::default() }); } @@ -5159,6 +5170,72 @@ mod stateful_tests { assert_eq!(response.status(), Status::Ok); } + #[test] + fn from_security_config_semantic_header_exclusions_reach_the_engine_config() { + let config = SecurityConfig { + excluded_semantic_detection_headers: [String::from("Cookie")].into_iter().collect(), + enabled_detection_categories: std::collections::BTreeSet::new(), + ..crate::SecurityConfig::default() + }; + let fairing = GuardFairing::from_security_config(&config).expect("valid config"); + let installed = fairing + .detection_exclusions + .as_ref() + .expect("the semantic header set alone installs the block"); + assert_eq!( + installed.excluded_semantic_detection_headers, + vec![String::from("Cookie")] + ); + } + + #[tokio::test] + async fn from_security_config_scan_time_budget_reaches_the_scan() { + let client = tracked_config( + GuardFairing::from_security_config(&crate::SecurityConfig::default()).expect("valid"), + ) + .await; + let response = client + .get("/hello") + .remote(peer_ip([203, 0, 113, 9])) + .dispatch() + .await; + assert_eq!( + response.status(), + Status::Ok, + "the default budget leaves it alone" + ); + + // A spent cap fails closed: the engine's scan answers a threat even + // for a benign request. + let spent = SecurityConfig { + detection_max_scan_seconds: 0.0, + ..crate::SecurityConfig::default() + }; + let client = + tracked_config(GuardFairing::from_security_config(&spent).expect("valid")).await; + let response = client + .get("/hello") + .remote(peer_ip([203, 0, 113, 9])) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + } + + #[test] + fn from_security_config_custom_scan_time_cap_alone_installs_the_block() { + let config = SecurityConfig { + enabled_detection_categories: std::collections::BTreeSet::new(), + detection_max_scan_seconds: 2.5, + ..crate::SecurityConfig::default() + }; + let fairing = GuardFairing::from_security_config(&config).expect("valid config"); + let installed = fairing + .detection_exclusions + .as_ref() + .expect("the cap installs it"); + assert_eq!(installed.max_scan_seconds, Some(2.5)); + } + #[tokio::test] async fn from_security_config_security_headers_and_cors_render() { let config = SecurityConfig {