1use std::collections::{BTreeMap, BTreeSet};
46use std::net::IpAddr;
47use std::path::PathBuf;
48use std::sync::Arc;
49
50use crate::behavior::BehaviorRule;
51use crate::cloud_fetch::CloudRangeStore;
52use crate::custom_checks::CustomRequestFn;
53use crate::geo::GeoIpHandler;
54use crate::headers_auth::AuthVerifier;
55use crate::ip_ban::{IpBanConfig, ThreatBanEntry};
56use crate::payload::{OnBlockHook, OnErrorFn, ResponseModifierFn};
57use crate::security_headers::SecurityHeadersConfig;
58
59pub const ALL_DETECTION_CATEGORIES: [&str; 19] = [
63 "xss",
64 "sqli",
65 "dir_traversal",
66 "path_traversal",
67 "cmd_injection",
68 "file_inclusion",
69 "ldap",
70 "xml",
71 "ssrf",
72 "nosql",
73 "file_upload",
74 "template",
75 "http_split",
76 "sensitive_file",
77 "cms_probing",
78 "recon",
79 "proto_pollution",
80 "code_injection",
81 "deserialization",
82];
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum LogLevel {
88 Info,
90 Debug,
92 Warning,
94 Error,
96 Critical,
98}
99
100impl LogLevel {
101 #[must_use]
103 pub const fn as_str(self) -> &'static str {
104 match self {
105 Self::Info => "INFO",
106 Self::Debug => "DEBUG",
107 Self::Warning => "WARNING",
108 Self::Error => "ERROR",
109 Self::Critical => "CRITICAL",
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum LogFormat {
117 Text,
119 Json,
121}
122
123impl LogFormat {
124 #[must_use]
126 pub const fn as_str(self) -> &'static str {
127 match self {
128 Self::Text => "text",
129 Self::Json => "json",
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum BufferOverflowPolicy {
139 Drop,
141 Block,
143 Raise,
145}
146
147impl BufferOverflowPolicy {
148 #[must_use]
150 pub const fn as_str(self) -> &'static str {
151 match self {
152 Self::Drop => "drop",
153 Self::Block => "block",
154 Self::Raise => "raise",
155 }
156 }
157}
158
159#[allow(clippy::struct_excessive_bools)]
167#[derive(Clone)]
168pub struct SecurityConfig {
169 pub trusted_proxies: Vec<String>,
173 pub trusted_proxy_depth: u32,
176 pub trust_x_forwarded_proto: bool,
178 pub passive_mode: bool,
180 pub geo_ip_handler: Option<Arc<dyn GeoIpHandler>>,
182 pub enable_redis: bool,
184 pub redis_url: Option<String>,
186 pub redis_prefix: String,
188 pub redis_socket_connect_timeout: Option<f64>,
190 pub redis_socket_timeout: Option<f64>,
192 pub redis_health_check_interval: u64,
194 pub redis_max_connections: Option<u64>,
196 pub redis_retries: u32,
198 pub whitelist: Option<Vec<String>>,
200 pub blacklist: Vec<String>,
202 pub exempt_ips: Vec<String>,
204 pub whitelist_countries: BTreeSet<String>,
206 pub blocked_countries: BTreeSet<String>,
208 pub blocked_user_agents: Vec<String>,
210 pub auto_ban_threshold: u32,
212 pub auto_ban_duration: u64,
214 pub threat_ban_config: BTreeMap<String, ThreatBanEntry>,
216 pub enable_rate_limit_auto_ban: bool,
218 pub global_behavior_rules: Vec<BehaviorRule>,
220 pub behavior_scan_response_body: bool,
222 pub behavior_max_response_body_inspect_bytes: usize,
224 pub body_read_timeout: f64,
226 pub sync_body_read_max_concurrent: usize,
228 pub custom_log_file: Option<String>,
230 pub log_suspicious_level: Option<LogLevel>,
232 pub log_request_level: Option<LogLevel>,
234 pub log_country_check_level: Option<LogLevel>,
236 pub log_format: LogFormat,
238 pub custom_error_responses: BTreeMap<u16, String>,
240 pub rate_limit: u32,
242 pub rate_limit_window: u64,
244 pub enforce_https: bool,
246 pub security_headers: SecurityHeadersConfig,
248 pub custom_request_check: Option<CustomRequestFn>,
250 pub auth_verifier: Option<AuthVerifier>,
252 pub custom_response_modifier: Option<ResponseModifierFn>,
254 pub on_block: Option<OnBlockHook>,
256 pub on_error: Option<OnErrorFn>,
258 pub enable_cors: bool,
260 pub cors_allow_origins: Vec<String>,
262 pub cors_allow_methods: Vec<String>,
264 pub cors_allow_headers: Vec<String>,
266 pub cors_allow_credentials: bool,
268 pub cors_expose_headers: Vec<String>,
270 pub cors_max_age: u64,
272 pub block_cloud_providers: Option<BTreeSet<String>>,
274 pub cloud_ip_refresh_interval: u64,
276 pub lazy_init: bool,
278 pub geo_ip_db_max_age: u64,
280 pub cloud_ip_store: Option<Arc<dyn CloudRangeStore>>,
282 pub exclude_paths: Vec<String>,
284 pub enable_ip_banning: bool,
286 pub enable_rate_limiting: bool,
288 pub enable_penetration_detection: bool,
290 pub fail_secure: bool,
292 pub redis_fail_open: bool,
294 pub route_resolution_strict: bool,
296 pub ipinfo_token: Option<String>,
298 pub ipinfo_db_path: Option<PathBuf>,
300 pub enable_agent: bool,
302 pub agent_api_key: Option<String>,
304 pub agent_strict: bool,
306 pub agent_endpoint: String,
308 pub agent_project_id: Option<String>,
310 pub agent_buffer_size: usize,
312 pub agent_flush_interval: u64,
314 pub agent_enable_events: bool,
316 pub agent_enable_metrics: bool,
318 pub agent_timeout: u64,
320 pub agent_retry_attempts: u32,
322 pub agent_project_encryption_key: Option<String>,
324 pub agent_guard_version: Option<String>,
326 pub agent_high_watermark_ratio: Option<f64>,
328 pub agent_max_concurrent_flushes: Option<u64>,
330 pub agent_buffer_overflow_policy: Option<BufferOverflowPolicy>,
332 pub agent_backoff_factor: Option<f64>,
334 pub agent_sensitive_headers: Option<Vec<String>>,
336 pub agent_max_payload_size: Option<u64>,
338 pub agent_compression_enabled: Option<bool>,
340 pub agent_compression_threshold: Option<u64>,
342 pub agent_install_id: Option<String>,
344 pub agent_payload_signing_secret: Option<String>,
346 pub enable_dynamic_rules: bool,
348 pub dynamic_rule_interval: u64,
350 pub dynamic_rules_cache_path: Option<PathBuf>,
352 pub agent_status_interval: u64,
354 pub emergency_mode: bool,
356 pub emergency_whitelist: Vec<String>,
358 pub endpoint_rate_limits: BTreeMap<String, (u32, u64)>,
360 pub detection_compiler_timeout: f64,
362 pub detection_pattern_validation_cache_path: Option<String>,
364 pub detection_max_content_length: usize,
366 pub detection_max_body_inspect_bytes: usize,
368 pub detection_max_scan_values: usize,
370 pub detection_max_scan_chars: usize,
372 pub detection_max_json_depth: usize,
374 pub detection_binary_min_run_length: usize,
376 pub detection_preserve_attack_patterns: bool,
378 pub detection_semantic_threshold: f64,
380 pub detection_anomaly_threshold: f64,
382 pub detection_slow_pattern_threshold: f64,
384 pub detection_monitor_history_size: usize,
386 pub detection_max_tracked_patterns: usize,
388 pub detection_anomaly_emission_cooldown: f64,
390 pub detection_min_samples_for_anomaly: usize,
392 pub detection_threat_score_threshold: f64,
394 pub muted_event_types: BTreeSet<String>,
396 pub muted_metric_types: BTreeSet<String>,
398 pub muted_check_logs: BTreeSet<String>,
400 pub log_sensitive_headers: BTreeSet<String>,
402 pub log_sensitive_params: BTreeSet<String>,
404 pub log_sensitive_body_fields: BTreeSet<String>,
406 pub enable_otel: bool,
408 pub otel_service_name: String,
410 pub otel_exporter_endpoint: Option<String>,
412 pub otel_resource_attributes: BTreeMap<String, String>,
414 pub enable_logfire: bool,
416 pub logfire_service_name: String,
418 pub enable_enrichment: bool,
420 pub excluded_detection_headers: BTreeSet<String>,
422 pub excluded_detection_params: BTreeSet<String>,
424 pub excluded_detection_body_fields: BTreeSet<String>,
426 pub detection_scan_body: bool,
428 pub enabled_detection_categories: BTreeSet<String>,
431}
432
433impl Default for SecurityConfig {
434 #[allow(clippy::too_many_lines)]
438 fn default() -> Self {
439 Self {
440 trusted_proxies: Vec::new(),
441 trusted_proxy_depth: 1,
442 trust_x_forwarded_proto: false,
443 passive_mode: false,
444 geo_ip_handler: None,
445 enable_redis: true,
446 redis_url: Some(String::from("redis://localhost:6379")),
447 redis_prefix: String::from("guard_core:"),
448 redis_socket_connect_timeout: Some(2.0),
449 redis_socket_timeout: Some(2.0),
450 redis_health_check_interval: 30,
451 redis_max_connections: None,
452 redis_retries: 1,
453 whitelist: None,
454 blacklist: Vec::new(),
455 exempt_ips: Vec::new(),
456 whitelist_countries: BTreeSet::new(),
457 blocked_countries: BTreeSet::new(),
458 blocked_user_agents: Vec::new(),
459 auto_ban_threshold: 10,
460 auto_ban_duration: 3600,
461 threat_ban_config: BTreeMap::new(),
462 enable_rate_limit_auto_ban: false,
463 global_behavior_rules: Vec::new(),
464 behavior_scan_response_body: false,
465 behavior_max_response_body_inspect_bytes: 262_144,
466 body_read_timeout: 3.0,
467 sync_body_read_max_concurrent: 64,
468 custom_log_file: None,
469 log_suspicious_level: Some(LogLevel::Warning),
470 log_request_level: None,
471 log_country_check_level: Some(LogLevel::Info),
472 log_format: LogFormat::Text,
473 custom_error_responses: BTreeMap::new(),
474 rate_limit: 10,
475 rate_limit_window: 60,
476 enforce_https: false,
477 security_headers: SecurityHeadersConfig::reference_default(),
478 custom_request_check: None,
479 auth_verifier: None,
480 custom_response_modifier: None,
481 on_block: None,
482 on_error: None,
483 enable_cors: false,
484 cors_allow_origins: vec![String::from("*")],
485 cors_allow_methods: vec![
486 String::from("GET"),
487 String::from("POST"),
488 String::from("PUT"),
489 String::from("PATCH"),
490 String::from("DELETE"),
491 String::from("OPTIONS"),
492 ],
493 cors_allow_headers: vec![String::from("*")],
494 cors_allow_credentials: false,
495 cors_expose_headers: Vec::new(),
496 cors_max_age: 600,
497 block_cloud_providers: None,
498 cloud_ip_refresh_interval: 3600,
499 lazy_init: true,
500 geo_ip_db_max_age: 86_400,
501 cloud_ip_store: None,
502 exclude_paths: vec![
503 String::from("/docs"),
504 String::from("/redoc"),
505 String::from("/openapi.json"),
506 String::from("/openapi.yaml"),
507 String::from("/favicon.ico"),
508 String::from("/static"),
509 ],
510 enable_ip_banning: true,
511 enable_rate_limiting: true,
512 enable_penetration_detection: true,
513 fail_secure: true,
514 redis_fail_open: false,
515 route_resolution_strict: false,
516 ipinfo_token: None,
517 ipinfo_db_path: Some(PathBuf::from("data/ipinfo/country_asn.mmdb")),
518 enable_agent: false,
519 agent_api_key: None,
520 agent_strict: false,
521 agent_endpoint: String::from("https://api.guard-core.com"),
522 agent_project_id: None,
523 agent_buffer_size: 100,
524 agent_flush_interval: 30,
525 agent_enable_events: true,
526 agent_enable_metrics: true,
527 agent_timeout: 30,
528 agent_retry_attempts: 3,
529 agent_project_encryption_key: None,
530 agent_guard_version: None,
531 agent_high_watermark_ratio: None,
532 agent_max_concurrent_flushes: None,
533 agent_buffer_overflow_policy: None,
534 agent_backoff_factor: None,
535 agent_sensitive_headers: None,
536 agent_max_payload_size: None,
537 agent_compression_enabled: None,
538 agent_compression_threshold: None,
539 agent_install_id: None,
540 agent_payload_signing_secret: None,
541 enable_dynamic_rules: false,
542 dynamic_rule_interval: 300,
543 dynamic_rules_cache_path: None,
544 agent_status_interval: 300,
545 emergency_mode: false,
546 emergency_whitelist: Vec::new(),
547 endpoint_rate_limits: BTreeMap::new(),
548 detection_compiler_timeout: 2.0,
549 detection_pattern_validation_cache_path: None,
550 detection_max_content_length: 10_000,
551 detection_max_body_inspect_bytes: 262_144,
552 detection_max_scan_values: 512,
553 detection_max_scan_chars: 65_536,
554 detection_max_json_depth: 32,
555 detection_binary_min_run_length: 16,
556 detection_preserve_attack_patterns: true,
557 detection_semantic_threshold: 0.7,
558 detection_anomaly_threshold: 3.0,
559 detection_slow_pattern_threshold: 0.1,
560 detection_monitor_history_size: 1000,
561 detection_max_tracked_patterns: 1000,
562 detection_anomaly_emission_cooldown: 60.0,
563 detection_min_samples_for_anomaly: 30,
564 detection_threat_score_threshold: 1.0,
565 muted_event_types: BTreeSet::new(),
566 muted_metric_types: BTreeSet::new(),
567 muted_check_logs: BTreeSet::new(),
568 log_sensitive_headers: BTreeSet::new(),
569 log_sensitive_params: BTreeSet::new(),
570 log_sensitive_body_fields: BTreeSet::new(),
571 enable_otel: false,
572 otel_service_name: String::from("guard-core"),
573 otel_exporter_endpoint: None,
574 otel_resource_attributes: BTreeMap::new(),
575 enable_logfire: false,
576 logfire_service_name: String::from("guard-core"),
577 enable_enrichment: false,
578 excluded_detection_headers: BTreeSet::new(),
579 excluded_detection_params: BTreeSet::new(),
580 excluded_detection_body_fields: BTreeSet::new(),
581 detection_scan_body: true,
582 enabled_detection_categories: ALL_DETECTION_CATEGORIES
583 .iter()
584 .map(|category| (*category).to_owned())
585 .collect(),
586 }
587 }
588}
589
590impl SecurityConfig {
591 #[must_use]
594 pub fn new() -> Self {
595 Self::default()
596 }
597
598 #[allow(clippy::too_many_lines)]
609 pub fn validate(&self) -> Result<(), SecurityConfigError> {
610 let mut problems = Vec::new();
611
612 if self.trusted_proxy_depth < 1 {
613 problems.push("trusted_proxy_depth must be at least 1".to_owned());
614 }
615 if self
616 .redis_socket_connect_timeout
617 .is_some_and(|value| value <= 0.0)
618 {
619 problems.push("redis_socket_connect_timeout must be positive when set".to_owned());
620 }
621 if self.redis_socket_timeout.is_some_and(|value| value <= 0.0) {
622 problems.push("redis_socket_timeout must be positive when set".to_owned());
623 }
624 if self.redis_max_connections.is_some_and(|value| value < 1) {
625 problems.push("redis_max_connections must be at least 1 when set".to_owned());
626 }
627 if self.auto_ban_threshold < 1 {
628 problems.push("auto_ban_threshold must be at least 1".to_owned());
629 }
630 if self.auto_ban_duration < 1 {
631 problems.push("auto_ban_duration must be at least 1".to_owned());
632 }
633 if !(1024..=10_485_760).contains(&self.behavior_max_response_body_inspect_bytes) {
634 problems.push(
635 "behavior_max_response_body_inspect_bytes must be between 1024 and 10485760"
636 .to_owned(),
637 );
638 }
639 if self.body_read_timeout <= 0.0 || self.body_read_timeout > 30.0 {
640 problems.push("body_read_timeout must be between 0 (exclusive) and 30".to_owned());
641 }
642 if !(1..=10_000).contains(&self.sync_body_read_max_concurrent) {
643 problems.push("sync_body_read_max_concurrent must be between 1 and 10000".to_owned());
644 }
645 if !(60..=86_400).contains(&self.cloud_ip_refresh_interval) {
646 problems.push("cloud_ip_refresh_interval must be between 60 and 86400".to_owned());
647 }
648 if !(3600..=604_800).contains(&self.geo_ip_db_max_age) {
649 problems.push("geo_ip_db_max_age must be between 3600 and 604800".to_owned());
650 }
651 if self.dynamic_rule_interval < 60 {
652 problems.push("dynamic_rule_interval must be at least 60".to_owned());
653 }
654 if !(60..=86_400).contains(&self.agent_status_interval) {
655 problems.push("agent_status_interval must be between 60 and 86400".to_owned());
656 }
657 if !(0.1..=10.0).contains(&self.detection_compiler_timeout) {
658 problems.push("detection_compiler_timeout must be between 0.1 and 10.0".to_owned());
659 }
660 if !(1000..=100_000).contains(&self.detection_max_content_length) {
661 problems
662 .push("detection_max_content_length must be between 1000 and 100000".to_owned());
663 }
664 if !(1024..=10_485_760).contains(&self.detection_max_body_inspect_bytes) {
665 problems.push(
666 "detection_max_body_inspect_bytes must be between 1024 and 10485760".to_owned(),
667 );
668 }
669 if !(2..=100_000).contains(&self.detection_max_scan_values) {
670 problems.push("detection_max_scan_values must be between 2 and 100000".to_owned());
671 }
672 if !(1024..=262_144).contains(&self.detection_max_scan_chars) {
673 problems.push("detection_max_scan_chars must be between 1024 and 262144".to_owned());
674 }
675 if !(1..=1000).contains(&self.detection_max_json_depth) {
676 problems.push("detection_max_json_depth must be between 1 and 1000".to_owned());
677 }
678 if !(4..=1024).contains(&self.detection_binary_min_run_length) {
679 problems.push("detection_binary_min_run_length must be between 4 and 1024".to_owned());
680 }
681 if !(0.0..=1.0).contains(&self.detection_semantic_threshold) {
682 problems.push("detection_semantic_threshold must be between 0.0 and 1.0".to_owned());
683 }
684 if !(1.0..=10.0).contains(&self.detection_anomaly_threshold) {
685 problems.push("detection_anomaly_threshold must be between 1.0 and 10.0".to_owned());
686 }
687 if !(0.01..=1.0).contains(&self.detection_slow_pattern_threshold) {
688 problems
689 .push("detection_slow_pattern_threshold must be between 0.01 and 1.0".to_owned());
690 }
691 if !(100..=10_000).contains(&self.detection_monitor_history_size) {
692 problems
693 .push("detection_monitor_history_size must be between 100 and 10000".to_owned());
694 }
695 if !(100..=5000).contains(&self.detection_max_tracked_patterns) {
696 problems.push("detection_max_tracked_patterns must be between 100 and 5000".to_owned());
697 }
698 if !(1.0..=3600.0).contains(&self.detection_anomaly_emission_cooldown) {
699 problems.push(
700 "detection_anomaly_emission_cooldown must be between 1.0 and 3600.0".to_owned(),
701 );
702 }
703 if !(10..=1000).contains(&self.detection_min_samples_for_anomaly) {
704 problems
705 .push("detection_min_samples_for_anomaly must be between 10 and 1000".to_owned());
706 }
707 if !(0.0..=10.0).contains(&self.detection_threat_score_threshold) {
708 problems
709 .push("detection_threat_score_threshold must be between 0.0 and 10.0".to_owned());
710 }
711 if self
712 .agent_high_watermark_ratio
713 .is_some_and(|ratio| ratio <= 0.0 || ratio > 1.0)
714 {
715 problems
716 .push("agent_high_watermark_ratio must be between 0 (exclusive) and 1".to_owned());
717 }
718 if self
719 .agent_max_concurrent_flushes
720 .is_some_and(|value| value < 1)
721 {
722 problems.push("agent_max_concurrent_flushes must be at least 1 when set".to_owned());
723 }
724 if self.agent_backoff_factor.is_some_and(|value| value <= 0.0) {
725 problems.push("agent_backoff_factor must be positive when set".to_owned());
726 }
727
728 for (field, entries) in [
729 ("whitelist", self.whitelist.as_deref().unwrap_or(&[])),
730 ("blacklist", &self.blacklist[..]),
731 ("exempt_ips", &self.exempt_ips[..]),
732 ] {
733 for entry in entries {
734 if parse_ip_or_cidr(entry).is_none() {
735 problems.push(format!("{field}: invalid IP or CIDR range ({entry})"));
736 }
737 }
738 }
739 for entry in &self.trusted_proxies {
740 if entry != "unix" && parse_ip_or_cidr(entry).is_none() {
741 problems.push(format!(
742 "trusted_proxies: invalid proxy IP or CIDR range ({entry})"
743 ));
744 }
745 }
746
747 if self.enable_agent && self.agent_api_key.is_none() {
748 problems.push("agent_api_key is required when enable_agent is true".to_owned());
749 }
750 if self.enable_dynamic_rules && !self.enable_agent {
751 problems.push("enable_agent must be true when enable_dynamic_rules is true".to_owned());
752 }
753 if self.enable_enrichment && !self.enable_agent {
754 problems.push("enable_enrichment requires enable_agent=true".to_owned());
755 }
756
757 if problems.is_empty() {
758 Ok(())
759 } else {
760 Err(SecurityConfigError { problems })
761 }
762 }
763
764 #[must_use]
767 pub fn ip_ban_config(&self) -> IpBanConfig {
768 IpBanConfig {
769 enable_ip_banning: self.enable_ip_banning,
770 auto_ban_threshold: self.auto_ban_threshold,
771 auto_ban_duration: self.auto_ban_duration,
772 threat_ban_config: self.threat_ban_config.clone().into_iter().collect(),
773 }
774 }
775}
776
777fn parse_ip_or_cidr(entry: &str) -> Option<IpAddr> {
781 if let Ok(ip) = entry.parse::<IpAddr>() {
782 return Some(ip);
783 }
784 let (address, prefix) = entry.split_once('/')?;
785 let prefix = prefix.parse::<u8>().ok()?;
786 let address = address.parse::<IpAddr>().ok()?;
787 let max_prefix = match address {
788 IpAddr::V4(_) => 32,
789 IpAddr::V6(_) => 128,
790 };
791 (prefix <= max_prefix).then_some(address)
792}
793
794#[derive(Debug, Clone, PartialEq, Eq)]
797pub struct SecurityConfigError {
798 pub problems: Vec<String>,
800}
801
802impl std::fmt::Display for SecurityConfigError {
803 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804 write!(
805 f,
806 "invalid SecurityConfig ({} problems)",
807 self.problems.len()
808 )
809 }
810}
811
812impl std::error::Error for SecurityConfigError {}
813
814#[cfg(test)]
815#[allow(clippy::field_reassign_with_default)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn defaults_satisfy_every_constraint() {
823 assert!(SecurityConfig::new().validate().is_ok());
826 assert!(SecurityConfig::default().validate().is_ok());
827 }
828
829 #[test]
830 #[allow(clippy::too_many_lines)]
831 fn every_reference_default_is_carried() {
832 let config = SecurityConfig::default();
833 assert!(config.trusted_proxies.is_empty());
834 assert_eq!(config.trusted_proxy_depth, 1);
835 assert!(!config.trust_x_forwarded_proto);
836 assert!(!config.passive_mode);
837 assert!(config.geo_ip_handler.is_none());
838 assert!(config.enable_redis);
839 assert_eq!(config.redis_url.as_deref(), Some("redis://localhost:6379"));
840 assert_eq!(config.redis_prefix, "guard_core:");
841 assert!(
842 config
843 .redis_socket_connect_timeout
844 .is_some_and(|value| (value - 2.0).abs() < f64::EPSILON)
845 );
846 assert!(
847 config
848 .redis_socket_timeout
849 .is_some_and(|value| (value - 2.0).abs() < f64::EPSILON)
850 );
851 assert_eq!(config.redis_health_check_interval, 30);
852 assert_eq!(config.redis_max_connections, None);
853 assert_eq!(config.redis_retries, 1);
854 assert_eq!(config.whitelist, None);
855 assert!(config.blacklist.is_empty());
856 assert!(config.exempt_ips.is_empty());
857 assert!(config.whitelist_countries.is_empty());
858 assert!(config.blocked_countries.is_empty());
859 assert!(config.blocked_user_agents.is_empty());
860 assert_eq!(config.auto_ban_threshold, 10);
861 assert_eq!(config.auto_ban_duration, 3600);
862 assert!(config.threat_ban_config.is_empty());
863 assert!(!config.enable_rate_limit_auto_ban);
864 assert!(config.global_behavior_rules.is_empty());
865 assert!(!config.behavior_scan_response_body);
866 assert_eq!(config.behavior_max_response_body_inspect_bytes, 262_144);
867 assert!((config.body_read_timeout - 3.0).abs() < f64::EPSILON);
868 assert_eq!(config.sync_body_read_max_concurrent, 64);
869 assert_eq!(config.custom_log_file, None);
870 assert_eq!(config.log_suspicious_level, Some(LogLevel::Warning));
871 assert_eq!(config.log_request_level, None);
872 assert_eq!(config.log_country_check_level, Some(LogLevel::Info));
873 assert_eq!(config.log_format, LogFormat::Text);
874 assert!(config.custom_error_responses.is_empty());
875 assert_eq!(config.rate_limit, 10);
876 assert_eq!(config.rate_limit_window, 60);
877 assert!(!config.enforce_https);
878 assert_eq!(
879 config.security_headers,
880 SecurityHeadersConfig::reference_default()
881 );
882 assert!(config.custom_request_check.is_none());
883 assert!(config.auth_verifier.is_none());
884 assert!(config.custom_response_modifier.is_none());
885 assert!(config.on_block.is_none());
886 assert!(config.on_error.is_none());
887 assert!(!config.enable_cors);
888 assert_eq!(config.cors_allow_origins, ["*"]);
889 assert_eq!(
890 config.cors_allow_methods,
891 ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
892 );
893 assert_eq!(config.cors_allow_headers, ["*"]);
894 assert!(!config.cors_allow_credentials);
895 assert!(config.cors_expose_headers.is_empty());
896 assert_eq!(config.cors_max_age, 600);
897 assert_eq!(config.block_cloud_providers, None);
898 assert_eq!(config.cloud_ip_refresh_interval, 3600);
899 assert!(config.lazy_init);
900 assert_eq!(config.geo_ip_db_max_age, 86_400);
901 assert!(config.cloud_ip_store.is_none());
902 assert_eq!(
903 config.exclude_paths,
904 [
905 "/docs",
906 "/redoc",
907 "/openapi.json",
908 "/openapi.yaml",
909 "/favicon.ico",
910 "/static"
911 ]
912 );
913 assert!(config.enable_ip_banning);
914 assert!(config.enable_rate_limiting);
915 assert!(config.enable_penetration_detection);
916 assert!(config.fail_secure);
917 assert!(!config.redis_fail_open);
918 assert!(!config.route_resolution_strict);
919 assert_eq!(config.ipinfo_token, None);
920 assert_eq!(
921 config.ipinfo_db_path.as_deref(),
922 Some(std::path::Path::new("data/ipinfo/country_asn.mmdb"))
923 );
924 assert!(!config.enable_agent);
925 assert_eq!(config.agent_api_key, None);
926 assert!(!config.agent_strict);
927 assert_eq!(config.agent_endpoint, "https://api.guard-core.com");
928 assert_eq!(config.agent_project_id, None);
929 assert_eq!(config.agent_buffer_size, 100);
930 assert_eq!(config.agent_flush_interval, 30);
931 assert!(config.agent_enable_events);
932 assert!(config.agent_enable_metrics);
933 assert_eq!(config.agent_timeout, 30);
934 assert_eq!(config.agent_retry_attempts, 3);
935 assert_eq!(config.agent_project_encryption_key, None);
936 assert_eq!(config.agent_guard_version, None);
937 assert_eq!(config.agent_high_watermark_ratio, None);
938 assert_eq!(config.agent_max_concurrent_flushes, None);
939 assert_eq!(config.agent_buffer_overflow_policy, None);
940 assert_eq!(config.agent_backoff_factor, None);
941 assert_eq!(config.agent_sensitive_headers, None);
942 assert_eq!(config.agent_max_payload_size, None);
943 assert_eq!(config.agent_compression_enabled, None);
944 assert_eq!(config.agent_compression_threshold, None);
945 assert_eq!(config.agent_install_id, None);
946 assert_eq!(config.agent_payload_signing_secret, None);
947 assert!(!config.enable_dynamic_rules);
948 assert_eq!(config.dynamic_rule_interval, 300);
949 assert_eq!(config.dynamic_rules_cache_path, None);
950 assert_eq!(config.agent_status_interval, 300);
951 assert!(!config.emergency_mode);
952 assert!(config.emergency_whitelist.is_empty());
953 assert!(config.endpoint_rate_limits.is_empty());
954 assert!((config.detection_compiler_timeout - 2.0).abs() < f64::EPSILON);
955 assert_eq!(config.detection_pattern_validation_cache_path, None);
956 assert_eq!(config.detection_max_content_length, 10_000);
957 assert_eq!(config.detection_max_body_inspect_bytes, 262_144);
958 assert_eq!(config.detection_max_scan_values, 512);
959 assert_eq!(config.detection_max_scan_chars, 65_536);
960 assert_eq!(config.detection_max_json_depth, 32);
961 assert_eq!(config.detection_binary_min_run_length, 16);
962 assert!(config.detection_preserve_attack_patterns);
963 assert!((config.detection_semantic_threshold - 0.7).abs() < f64::EPSILON);
964 assert!((config.detection_anomaly_threshold - 3.0).abs() < f64::EPSILON);
965 assert!((config.detection_slow_pattern_threshold - 0.1).abs() < f64::EPSILON);
966 assert_eq!(config.detection_monitor_history_size, 1000);
967 assert_eq!(config.detection_max_tracked_patterns, 1000);
968 assert!((config.detection_anomaly_emission_cooldown - 60.0).abs() < f64::EPSILON);
969 assert_eq!(config.detection_min_samples_for_anomaly, 30);
970 assert!((config.detection_threat_score_threshold - 1.0).abs() < f64::EPSILON);
971 assert!(config.muted_event_types.is_empty());
972 assert!(config.muted_metric_types.is_empty());
973 assert!(config.muted_check_logs.is_empty());
974 assert!(config.log_sensitive_headers.is_empty());
975 assert!(config.log_sensitive_params.is_empty());
976 assert!(config.log_sensitive_body_fields.is_empty());
977 assert!(!config.enable_otel);
978 assert_eq!(config.otel_service_name, "guard-core");
979 assert_eq!(config.otel_exporter_endpoint, None);
980 assert!(config.otel_resource_attributes.is_empty());
981 assert!(!config.enable_logfire);
982 assert_eq!(config.logfire_service_name, "guard-core");
983 assert!(!config.enable_enrichment);
984 assert!(config.excluded_detection_headers.is_empty());
985 assert!(config.excluded_detection_params.is_empty());
986 assert!(config.excluded_detection_body_fields.is_empty());
987 assert!(config.detection_scan_body);
988 assert_eq!(
989 config.enabled_detection_categories,
990 ALL_DETECTION_CATEGORIES
991 .iter()
992 .map(|category| (*category).to_owned())
993 .collect::<BTreeSet<String>>()
994 );
995 }
996
997 #[test]
998 fn the_category_table_is_the_reference_set() {
999 assert_eq!(ALL_DETECTION_CATEGORIES.len(), 19);
1000 assert!(ALL_DETECTION_CATEGORIES.contains(&"xss"));
1001 assert!(ALL_DETECTION_CATEGORIES.contains(&"deserialization"));
1002 }
1003
1004 #[test]
1005 fn the_log_level_round_trips_the_reference_literals() {
1006 assert_eq!(LogLevel::Info.as_str(), "INFO");
1007 assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
1008 assert_eq!(LogLevel::Warning.as_str(), "WARNING");
1009 assert_eq!(LogLevel::Error.as_str(), "ERROR");
1010 assert_eq!(LogLevel::Critical.as_str(), "CRITICAL");
1011 }
1012
1013 #[test]
1014 fn the_log_format_round_trips_the_reference_literals() {
1015 assert_eq!(LogFormat::Text.as_str(), "text");
1016 assert_eq!(LogFormat::Json.as_str(), "json");
1017 }
1018
1019 #[test]
1020 fn the_overflow_policy_round_trips_the_reference_literals() {
1021 assert_eq!(BufferOverflowPolicy::Drop.as_str(), "drop");
1022 assert_eq!(BufferOverflowPolicy::Block.as_str(), "block");
1023 assert_eq!(BufferOverflowPolicy::Raise.as_str(), "raise");
1024 }
1025
1026 #[test]
1029 #[allow(clippy::too_many_lines)]
1030 fn every_bound_constraint_names_its_field() {
1031 type Mutator = Box<dyn Fn(&mut SecurityConfig)>;
1032 let cases: Vec<(&str, Mutator)> = vec![
1033 (
1034 "trusted_proxy_depth",
1035 Box::new(|config: &mut SecurityConfig| config.trusted_proxy_depth = 0),
1036 ),
1037 (
1038 "redis_socket_connect_timeout",
1039 Box::new(|config: &mut SecurityConfig| {
1040 config.redis_socket_connect_timeout = Some(0.0);
1041 }),
1042 ),
1043 (
1044 "redis_socket_timeout",
1045 Box::new(|config: &mut SecurityConfig| config.redis_socket_timeout = Some(-1.0)),
1046 ),
1047 (
1048 "redis_max_connections",
1049 Box::new(|config: &mut SecurityConfig| config.redis_max_connections = Some(0)),
1050 ),
1051 (
1052 "auto_ban_threshold",
1053 Box::new(|config: &mut SecurityConfig| config.auto_ban_threshold = 0),
1054 ),
1055 (
1056 "auto_ban_duration",
1057 Box::new(|config: &mut SecurityConfig| config.auto_ban_duration = 0),
1058 ),
1059 (
1060 "behavior_max_response_body_inspect_bytes",
1061 Box::new(|config: &mut SecurityConfig| {
1062 config.behavior_max_response_body_inspect_bytes = 1023;
1063 }),
1064 ),
1065 (
1066 "body_read_timeout",
1067 Box::new(|config: &mut SecurityConfig| config.body_read_timeout = 0.0),
1068 ),
1069 (
1070 "body_read_timeout",
1071 Box::new(|config: &mut SecurityConfig| config.body_read_timeout = 30.1),
1072 ),
1073 (
1074 "sync_body_read_max_concurrent",
1075 Box::new(|config: &mut SecurityConfig| {
1076 config.sync_body_read_max_concurrent = 0;
1077 }),
1078 ),
1079 (
1080 "cloud_ip_refresh_interval",
1081 Box::new(|config: &mut SecurityConfig| config.cloud_ip_refresh_interval = 59),
1082 ),
1083 (
1084 "geo_ip_db_max_age",
1085 Box::new(|config: &mut SecurityConfig| config.geo_ip_db_max_age = 3599),
1086 ),
1087 (
1088 "dynamic_rule_interval",
1089 Box::new(|config: &mut SecurityConfig| config.dynamic_rule_interval = 59),
1090 ),
1091 (
1092 "agent_status_interval",
1093 Box::new(|config: &mut SecurityConfig| config.agent_status_interval = 59),
1094 ),
1095 (
1096 "detection_compiler_timeout",
1097 Box::new(|config: &mut SecurityConfig| config.detection_compiler_timeout = 0.0),
1098 ),
1099 (
1100 "detection_max_content_length",
1101 Box::new(|config: &mut SecurityConfig| config.detection_max_content_length = 999),
1102 ),
1103 (
1104 "detection_max_body_inspect_bytes",
1105 Box::new(|config: &mut SecurityConfig| {
1106 config.detection_max_body_inspect_bytes = 1023;
1107 }),
1108 ),
1109 (
1110 "detection_max_scan_values",
1111 Box::new(|config: &mut SecurityConfig| config.detection_max_scan_values = 1),
1112 ),
1113 (
1114 "detection_max_scan_chars",
1115 Box::new(|config: &mut SecurityConfig| config.detection_max_scan_chars = 1023),
1116 ),
1117 (
1118 "detection_max_json_depth",
1119 Box::new(|config: &mut SecurityConfig| config.detection_max_json_depth = 0),
1120 ),
1121 (
1122 "detection_binary_min_run_length",
1123 Box::new(|config: &mut SecurityConfig| {
1124 config.detection_binary_min_run_length = 3;
1125 }),
1126 ),
1127 (
1128 "detection_semantic_threshold",
1129 Box::new(|config: &mut SecurityConfig| config.detection_semantic_threshold = 1.1),
1130 ),
1131 (
1132 "detection_anomaly_threshold",
1133 Box::new(|config: &mut SecurityConfig| config.detection_anomaly_threshold = 0.9),
1134 ),
1135 (
1136 "detection_slow_pattern_threshold",
1137 Box::new(|config: &mut SecurityConfig| {
1138 config.detection_slow_pattern_threshold = 0.0;
1139 }),
1140 ),
1141 (
1142 "detection_monitor_history_size",
1143 Box::new(|config: &mut SecurityConfig| {
1144 config.detection_monitor_history_size = 99;
1145 }),
1146 ),
1147 (
1148 "detection_max_tracked_patterns",
1149 Box::new(|config: &mut SecurityConfig| {
1150 config.detection_max_tracked_patterns = 99;
1151 }),
1152 ),
1153 (
1154 "detection_anomaly_emission_cooldown",
1155 Box::new(|config: &mut SecurityConfig| {
1156 config.detection_anomaly_emission_cooldown = 0.9;
1157 }),
1158 ),
1159 (
1160 "detection_min_samples_for_anomaly",
1161 Box::new(|config: &mut SecurityConfig| {
1162 config.detection_min_samples_for_anomaly = 9;
1163 }),
1164 ),
1165 (
1166 "detection_threat_score_threshold",
1167 Box::new(|config: &mut SecurityConfig| {
1168 config.detection_threat_score_threshold = 10.1;
1169 }),
1170 ),
1171 (
1172 "agent_high_watermark_ratio",
1173 Box::new(|config: &mut SecurityConfig| {
1174 config.agent_high_watermark_ratio = Some(1.1);
1175 }),
1176 ),
1177 (
1178 "agent_max_concurrent_flushes",
1179 Box::new(|config: &mut SecurityConfig| {
1180 config.agent_max_concurrent_flushes = Some(0);
1181 }),
1182 ),
1183 (
1184 "agent_backoff_factor",
1185 Box::new(|config: &mut SecurityConfig| config.agent_backoff_factor = Some(0.0)),
1186 ),
1187 ];
1188 for (field, mutate) in cases {
1189 let mut config = SecurityConfig::default();
1190 mutate(&mut config);
1191 let error = config.validate().unwrap_err();
1192 assert!(
1193 error.problems.iter().any(|problem| problem.contains(field)),
1194 "{field}: {:?}",
1195 error.problems
1196 );
1197 }
1198 }
1199
1200 #[test]
1201 fn the_ip_lists_reject_garbage_and_accept_ips_and_cidrs() {
1202 let mut config = SecurityConfig::default();
1203 config.whitelist = Some(vec![String::from("10.0.0.1"), String::from("10.0.0.0/8")]);
1204 config.blacklist = vec![String::from("2001:db8::1"), String::from("2001:db8::/32")];
1205 config.exempt_ips = vec![String::from("192.0.2.1")];
1206 config.trusted_proxies = vec![String::from("unix"), String::from("10.1.0.0/16")];
1207 assert!(config.validate().is_ok());
1208
1209 config.exempt_ips = vec![String::from("not-an-ip")];
1210 let error = config.validate().unwrap_err();
1211 assert!(
1212 error
1213 .problems
1214 .iter()
1215 .any(|problem| problem.contains("exempt_ips"))
1216 );
1217
1218 config.trusted_proxies = vec![String::from("also-bad")];
1219 config.whitelist = Some(vec![String::from("10.0.0.0/99")]);
1220 let error = config.validate().unwrap_err();
1221 assert!(
1222 error
1223 .problems
1224 .iter()
1225 .any(|problem| problem.contains("trusted_proxies"))
1226 );
1227 assert!(
1228 error
1229 .problems
1230 .iter()
1231 .any(|problem| problem.contains("whitelist"))
1232 );
1233 }
1234
1235 #[test]
1236 fn the_agent_cross_rules_hold() {
1237 let mut config = SecurityConfig::default();
1238 config.enable_agent = true;
1239 let error = config.validate().unwrap_err();
1240 assert!(
1241 error
1242 .problems
1243 .iter()
1244 .any(|problem| problem.contains("agent_api_key"))
1245 );
1246
1247 let mut config = SecurityConfig::default();
1248 config.enable_dynamic_rules = true;
1249 let error = config.validate().unwrap_err();
1250 assert!(
1251 error
1252 .problems
1253 .iter()
1254 .any(|problem| problem.contains("enable_agent"))
1255 );
1256
1257 let mut config = SecurityConfig::default();
1258 config.enable_enrichment = true;
1259 let error = config.validate().unwrap_err();
1260 assert!(
1261 error
1262 .problems
1263 .iter()
1264 .any(|problem| problem.contains("enable_enrichment"))
1265 );
1266
1267 let mut config = SecurityConfig::default();
1269 config.enable_agent = true;
1270 config.agent_api_key = Some(String::from("k"));
1271 config.enable_dynamic_rules = true;
1272 config.enable_enrichment = true;
1273 assert!(config.validate().is_ok());
1274 }
1275
1276 #[test]
1277 fn validation_collects_every_problem_at_once() {
1278 let mut config = SecurityConfig::default();
1279 config.auto_ban_threshold = 0;
1280 config.auto_ban_duration = 0;
1281 config.enable_dynamic_rules = true;
1282 let error = config.validate().unwrap_err();
1283 assert_eq!(error.problems.len(), 3);
1284 assert_eq!(error.to_string(), "invalid SecurityConfig (3 problems)");
1285 }
1286
1287 #[test]
1288 fn the_ban_group_maps_onto_the_engine_config() {
1289 let mut config = SecurityConfig::default();
1290 config.enable_ip_banning = false;
1291 config.auto_ban_threshold = 5;
1292 config.auto_ban_duration = 60;
1293 config.threat_ban_config.insert(
1294 String::from("rate_limit"),
1295 ThreatBanEntry {
1296 threshold: 3,
1297 duration: 120,
1298 },
1299 );
1300 let ban_config = config.ip_ban_config();
1301 assert!(!ban_config.enable_ip_banning);
1302 assert_eq!(ban_config.auto_ban_threshold, 5);
1303 assert_eq!(ban_config.auto_ban_duration, 60);
1304 assert_eq!(ban_config.threat_ban_config.len(), 1);
1305 }
1306}