use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub audit_all_requests: bool,
#[serde(default = "default_true")]
pub audit_auth_events: bool,
#[serde(default = "default_true")]
pub audit_config_events: bool,
#[serde(default)]
pub syslog: SyslogConfig,
#[serde(default)]
pub otlp_logs_enabled: bool,
#[serde(default)]
pub audited_routes: Vec<String>,
#[serde(default = "default_excluded_routes")]
pub excluded_routes: Vec<String>,
#[serde(default)]
pub retention_days: Option<u32>,
#[serde(default)]
pub archive_path: Option<String>,
#[serde(default = "default_cleanup_interval")]
pub cleanup_interval_hours: u32,
#[serde(default)]
pub alerts: Option<AlertConfig>,
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
enabled: true,
audit_all_requests: false,
audit_auth_events: true,
audit_config_events: true,
syslog: SyslogConfig::default(),
otlp_logs_enabled: false,
audited_routes: Vec::new(),
excluded_routes: default_excluded_routes(),
retention_days: None,
archive_path: None,
cleanup_interval_hours: default_cleanup_interval(),
alerts: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_threshold_secs")]
pub threshold_secs: u64,
#[serde(default = "default_cooldown_secs")]
pub cooldown_secs: u64,
#[serde(default = "default_true")]
pub notify_recovery: bool,
#[serde(default)]
pub webhooks: Vec<WebhookAlertConfig>,
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
enabled: false,
threshold_secs: default_threshold_secs(),
cooldown_secs: default_cooldown_secs(),
notify_recovery: true,
webhooks: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookAlertConfig {
pub url: String,
#[serde(default = "default_webhook_timeout_secs")]
pub timeout_secs: u64,
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyslogConfig {
#[serde(default = "default_syslog_transport")]
pub transport: String,
#[serde(default = "default_syslog_address")]
pub address: String,
#[serde(default = "default_syslog_facility")]
pub facility: u8,
#[serde(default)]
pub app_name: Option<String>,
}
impl Default for SyslogConfig {
fn default() -> Self {
Self {
transport: default_syslog_transport(),
address: default_syslog_address(),
facility: default_syslog_facility(),
app_name: None,
}
}
}
fn default_true() -> bool {
true
}
fn default_excluded_routes() -> Vec<String> {
vec![
"/health".to_string(),
"/ready".to_string(),
"/metrics".to_string(),
]
}
fn default_syslog_transport() -> String {
"udp".to_string()
}
fn default_syslog_address() -> String {
"127.0.0.1:514".to_string()
}
fn default_syslog_facility() -> u8 {
13 }
fn default_cleanup_interval() -> u32 {
24
}
fn default_threshold_secs() -> u64 {
30
}
fn default_cooldown_secs() -> u64 {
300
}
fn default_webhook_timeout_secs() -> u64 {
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_config_defaults() {
let config = AuditConfig::default();
assert!(config.enabled);
assert!(!config.audit_all_requests);
assert!(config.audit_auth_events);
assert!(!config.otlp_logs_enabled);
assert!(config.audited_routes.is_empty());
assert_eq!(
config.excluded_routes,
vec!["/health", "/ready", "/metrics"]
);
assert!(config.retention_days.is_none());
assert!(config.archive_path.is_none());
assert_eq!(config.cleanup_interval_hours, 24);
assert!(config.alerts.is_none());
}
#[test]
fn test_syslog_config_defaults() {
let config = SyslogConfig::default();
assert_eq!(config.transport, "udp");
assert_eq!(config.address, "127.0.0.1:514");
assert_eq!(config.facility, 13);
assert!(config.app_name.is_none());
}
#[test]
fn test_audit_config_serde_roundtrip() {
let config = AuditConfig {
enabled: true,
audit_all_requests: true,
audit_auth_events: false,
audit_config_events: true,
syslog: SyslogConfig {
transport: "tcp".to_string(),
address: "syslog.example.com:514".to_string(),
facility: 10,
app_name: Some("my-service".to_string()),
},
otlp_logs_enabled: true,
audited_routes: vec!["/api/v1/admin/*".to_string()],
excluded_routes: vec!["/health".to_string()],
retention_days: Some(90),
archive_path: Some("/var/audit/archive".to_string()),
cleanup_interval_hours: 12,
alerts: None,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: AuditConfig = serde_json::from_str(&json).unwrap();
assert!(deserialized.audit_all_requests);
assert!(!deserialized.audit_auth_events);
assert_eq!(deserialized.syslog.transport, "tcp");
assert_eq!(deserialized.syslog.facility, 10);
assert!(deserialized.otlp_logs_enabled);
assert_eq!(deserialized.audited_routes, vec!["/api/v1/admin/*"]);
assert_eq!(deserialized.retention_days, Some(90));
assert_eq!(
deserialized.archive_path,
Some("/var/audit/archive".to_string())
);
assert_eq!(deserialized.cleanup_interval_hours, 12);
}
#[test]
fn test_retention_fields_default_from_json() {
let json = r#"{"enabled": true}"#;
let config: AuditConfig = serde_json::from_str(json).unwrap();
assert!(config.retention_days.is_none());
assert!(config.archive_path.is_none());
assert_eq!(config.cleanup_interval_hours, 24);
}
#[test]
fn test_alert_config_defaults() {
let config = AlertConfig::default();
assert!(!config.enabled);
assert_eq!(config.threshold_secs, 30);
assert_eq!(config.cooldown_secs, 300);
assert!(config.notify_recovery);
assert!(config.webhooks.is_empty());
}
#[test]
fn test_alert_config_serde_roundtrip() {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
let config = AlertConfig {
enabled: true,
threshold_secs: 15,
cooldown_secs: 120,
notify_recovery: false,
webhooks: vec![WebhookAlertConfig {
url: "https://hooks.slack.com/test".to_string(),
timeout_secs: 5,
headers: headers.clone(),
}],
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: AlertConfig = serde_json::from_str(&json).unwrap();
assert!(deserialized.enabled);
assert_eq!(deserialized.threshold_secs, 15);
assert_eq!(deserialized.cooldown_secs, 120);
assert!(!deserialized.notify_recovery);
assert_eq!(deserialized.webhooks.len(), 1);
assert_eq!(deserialized.webhooks[0].url, "https://hooks.slack.com/test");
assert_eq!(deserialized.webhooks[0].timeout_secs, 5);
assert_eq!(
deserialized.webhooks[0]
.headers
.get("Authorization")
.unwrap(),
"Bearer test-token"
);
}
#[test]
fn test_audit_config_with_alerts_json() {
let json_str = r#"{
"enabled": true,
"audit_all_requests": false,
"audit_auth_events": true,
"otlp_logs_enabled": false,
"syslog": {
"transport": "udp",
"address": "127.0.0.1:514",
"facility": 13
},
"alerts": {
"enabled": true,
"threshold_secs": 30,
"cooldown_secs": 300,
"notify_recovery": true,
"webhooks": [
{
"url": "https://hooks.slack.com/services/T00/B00/xxx",
"timeout_secs": 10
}
]
}
}"#;
let config: AuditConfig = serde_json::from_str(json_str).unwrap();
assert!(config.enabled);
let alerts = config.alerts.unwrap();
assert!(alerts.enabled);
assert_eq!(alerts.threshold_secs, 30);
assert_eq!(alerts.cooldown_secs, 300);
assert!(alerts.notify_recovery);
assert_eq!(alerts.webhooks.len(), 1);
assert_eq!(
alerts.webhooks[0].url,
"https://hooks.slack.com/services/T00/B00/xxx"
);
}
}