use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ShadowMirrorConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_min_risk_score")]
pub min_risk_score: f32,
#[serde(default = "default_max_risk_score")]
pub max_risk_score: f32,
#[serde(default)]
pub honeypot_urls: Vec<String>,
#[serde(default = "default_sampling_rate")]
pub sampling_rate: f32,
#[serde(default = "default_per_ip_rate_limit")]
pub per_ip_rate_limit: u32,
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
#[serde(default)]
pub hmac_secret: Option<String>,
#[serde(default = "default_include_body")]
pub include_body: bool,
#[serde(default = "default_max_body_size")]
pub max_body_size: usize,
#[serde(default = "default_include_headers")]
pub include_headers: Vec<String>,
}
fn default_min_risk_score() -> f32 {
40.0
}
fn default_max_risk_score() -> f32 {
70.0
}
fn default_sampling_rate() -> f32 {
1.0
}
fn default_per_ip_rate_limit() -> u32 {
10
}
fn default_timeout_secs() -> u64 {
5
}
fn default_include_body() -> bool {
true
}
fn default_max_body_size() -> usize {
1024 * 1024 }
fn default_include_headers() -> Vec<String> {
vec![
"User-Agent".to_string(),
"Referer".to_string(),
"Origin".to_string(),
"Accept".to_string(),
"Accept-Language".to_string(),
"Accept-Encoding".to_string(),
]
}
impl Default for ShadowMirrorConfig {
fn default() -> Self {
Self {
enabled: false,
min_risk_score: default_min_risk_score(),
max_risk_score: default_max_risk_score(),
honeypot_urls: Vec::new(),
sampling_rate: default_sampling_rate(),
per_ip_rate_limit: default_per_ip_rate_limit(),
timeout_secs: default_timeout_secs(),
hmac_secret: None,
include_body: default_include_body(),
max_body_size: default_max_body_size(),
include_headers: default_include_headers(),
}
}
}
impl ShadowMirrorConfig {
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.timeout_secs)
}
pub fn validate(&self) -> Result<(), ShadowConfigError> {
if self.enabled && self.honeypot_urls.is_empty() {
return Err(ShadowConfigError::NoHoneypotUrls);
}
if self.min_risk_score >= self.max_risk_score {
return Err(ShadowConfigError::InvalidRiskRange {
min: self.min_risk_score,
max: self.max_risk_score,
});
}
if self.sampling_rate < 0.0 || self.sampling_rate > 1.0 {
return Err(ShadowConfigError::InvalidSamplingRate(self.sampling_rate));
}
for url in &self.honeypot_urls {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ShadowConfigError::InvalidHoneypotUrl(url.clone()));
}
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ShadowConfigError {
#[error("shadow mirroring enabled but no honeypot URLs configured")]
NoHoneypotUrls,
#[error("invalid risk score range: min ({min}) must be less than max ({max})")]
InvalidRiskRange { min: f32, max: f32 },
#[error("invalid sampling rate: {0} (must be 0.0-1.0)")]
InvalidSamplingRate(f32),
#[error("invalid honeypot URL: {0} (must start with http:// or https://)")]
InvalidHoneypotUrl(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ShadowMirrorConfig::default();
assert!(!config.enabled);
assert_eq!(config.min_risk_score, 40.0);
assert_eq!(config.max_risk_score, 70.0);
assert_eq!(config.sampling_rate, 1.0);
assert_eq!(config.per_ip_rate_limit, 10);
assert!(config.include_body);
}
#[test]
fn test_validate_disabled_without_urls() {
let config = ShadowMirrorConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_enabled_without_urls() {
let mut config = ShadowMirrorConfig::default();
config.enabled = true;
assert!(matches!(
config.validate(),
Err(ShadowConfigError::NoHoneypotUrls)
));
}
#[test]
fn test_validate_invalid_risk_range() {
let mut config = ShadowMirrorConfig::default();
config.min_risk_score = 70.0;
config.max_risk_score = 40.0;
assert!(matches!(
config.validate(),
Err(ShadowConfigError::InvalidRiskRange { .. })
));
}
#[test]
fn test_validate_invalid_sampling_rate() {
let mut config = ShadowMirrorConfig::default();
config.sampling_rate = 1.5;
assert!(matches!(
config.validate(),
Err(ShadowConfigError::InvalidSamplingRate(_))
));
}
#[test]
fn test_validate_invalid_honeypot_url() {
let mut config = ShadowMirrorConfig::default();
config.enabled = true;
config.honeypot_urls = vec!["not-a-url".to_string()];
assert!(matches!(
config.validate(),
Err(ShadowConfigError::InvalidHoneypotUrl(_))
));
}
#[test]
fn test_validate_valid_config() {
let mut config = ShadowMirrorConfig::default();
config.enabled = true;
config.honeypot_urls = vec!["https://honeypot.example.com/mirror".to_string()];
assert!(config.validate().is_ok());
}
#[test]
fn test_timeout_duration() {
let config = ShadowMirrorConfig::default();
assert_eq!(config.timeout(), Duration::from_secs(5));
}
}