#[cfg(feature = "audit-log")]
use chrono::{DateTime, Utc};
#[cfg(feature = "audit-log")]
use hmac::{Hmac, KeyInit, Mac};
#[cfg(feature = "audit-log")]
use log::{error, info, trace, warn};
#[cfg(feature = "audit-log")]
use secrecy::{ExposeSecret, SecretString};
#[cfg(feature = "audit-log")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "audit-log")]
use sha2::Sha256;
#[cfg(feature = "audit-log")]
use std::sync::Arc;
#[cfg(feature = "audit-log")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "audit-log")]
use std::time::Duration;
#[cfg(feature = "audit-log")]
use tokio::sync::mpsc::{self, Sender};
#[cfg(feature = "audit-log")]
use crate::error::LimiteronError;
#[cfg(feature = "audit-log")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type")]
pub enum AuditEvent {
Decision {
timestamp: DateTime<Utc>,
identifier: String,
decision: String,
reason: String,
request_id: Option<String>,
},
ConfigChange {
timestamp: DateTime<Utc>,
old_version: String,
new_version: String,
changes: Vec<String>,
operator: Option<String>,
},
BanOperation {
timestamp: DateTime<Utc>,
target: String,
action: String,
reason: String,
operator: String,
expires_at: Option<DateTime<Utc>>,
},
SystemEvent {
timestamp: DateTime<Utc>,
level: String,
name: String,
details: String,
},
ErrorEvent {
timestamp: DateTime<Utc>,
error_type: String,
message: String,
stack_trace: Option<String>,
},
}
#[cfg(feature = "audit-log")]
impl AuditEvent {
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
AuditEvent::Decision { timestamp, .. } => *timestamp,
AuditEvent::ConfigChange { timestamp, .. } => *timestamp,
AuditEvent::BanOperation { timestamp, .. } => *timestamp,
AuditEvent::SystemEvent { timestamp, .. } => *timestamp,
AuditEvent::ErrorEvent { timestamp, .. } => *timestamp,
}
}
pub fn operation(&self) -> &str {
match self {
AuditEvent::Decision { .. } => "decision",
AuditEvent::ConfigChange { .. } => "config_change",
AuditEvent::BanOperation { .. } => "ban_operation",
AuditEvent::SystemEvent { .. } => "system_event",
AuditEvent::ErrorEvent { .. } => "error_event",
}
}
pub fn target(&self) -> String {
match self {
AuditEvent::Decision { identifier, .. } => identifier.clone(),
AuditEvent::ConfigChange {
old_version,
new_version,
..
} => {
format!("{}->{}", old_version, new_version)
}
AuditEvent::BanOperation { target, .. } => target.clone(),
AuditEvent::SystemEvent { name, .. } => name.clone(),
AuditEvent::ErrorEvent { error_type, .. } => error_type.clone(),
}
}
pub fn result(&self) -> String {
match self {
AuditEvent::Decision { decision, .. } => decision.clone(),
AuditEvent::ConfigChange { changes, .. } => changes.join(","),
AuditEvent::BanOperation { action, .. } => action.clone(),
AuditEvent::SystemEvent { level, .. } => level.clone(),
AuditEvent::ErrorEvent { message, .. } => message.clone(),
}
}
}
#[cfg(feature = "audit-log")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLogEntry {
pub event: AuditEvent,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature_version: Option<u32>,
}
#[cfg(feature = "audit-log")]
impl AuditLogEntry {
pub fn new(event: AuditEvent) -> Self {
Self {
event,
signature: None,
signature_version: None,
}
}
pub fn with_signature(event: AuditEvent, signing_key: &str) -> Self {
let mut entry = Self::new(event);
entry.sign(signing_key);
entry
}
pub fn sign(&mut self, signing_key: &str) {
let signature = Self::generate_signature(&self.event, signing_key);
self.signature = Some(signature);
self.signature_version = Some(1); }
fn generate_signature(event: &AuditEvent, signing_key: &str) -> String {
let message = format!(
"{}|{}|{}|{}",
event.timestamp().to_rfc3339(),
event.operation(),
event.target(),
event.result()
);
let mut mac = Hmac::<Sha256>::new_from_slice(signing_key.as_bytes())
.expect("HMAC can take key of any size");
mac.update(message.as_bytes());
let result = mac.finalize();
hex::encode(result.into_bytes())
}
pub fn verify(&self, signing_key: &str) -> Result<bool, LimiteronError> {
let stored_signature = match &self.signature {
Some(sig) => sig,
None => {
return Err(LimiteronError::AuditLogError(
"日志条目缺少签名".to_string(),
));
}
};
let expected_signature = Self::generate_signature(&self.event, signing_key);
if Self::constant_time_compare(stored_signature, &expected_signature) {
Ok(true)
} else {
Err(LimiteronError::AuditLogError(
"签名验证失败,日志可能被篡改".to_string(),
))
}
}
fn constant_time_compare(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
let mut result = 0u8;
for (x, y) in a_bytes.iter().zip(b_bytes.iter()) {
result |= x ^ y;
}
result == 0
}
}
#[cfg(feature = "audit-log")]
#[derive(Debug, Default)]
pub struct AuditLogStats {
total_events: AtomicU64,
decision_events: AtomicU64,
config_change_events: AtomicU64,
ban_operation_events: AtomicU64,
system_events: AtomicU64,
error_events: AtomicU64,
batch_writes: AtomicU64,
write_failures: AtomicU64,
signature_failures: AtomicU64,
verification_failures: AtomicU64,
}
#[cfg(feature = "audit-log")]
impl AuditLogStats {
pub fn total_events(&self) -> u64 {
self.total_events.load(Ordering::Relaxed)
}
pub fn decision_events(&self) -> u64 {
self.decision_events.load(Ordering::Relaxed)
}
pub fn config_change_events(&self) -> u64 {
self.config_change_events.load(Ordering::Relaxed)
}
pub fn ban_operation_events(&self) -> u64 {
self.ban_operation_events.load(Ordering::Relaxed)
}
pub fn system_events(&self) -> u64 {
self.system_events.load(Ordering::Relaxed)
}
pub fn error_events(&self) -> u64 {
self.error_events.load(Ordering::Relaxed)
}
pub fn batch_writes(&self) -> u64 {
self.batch_writes.load(Ordering::Relaxed)
}
pub fn write_failures(&self) -> u64 {
self.write_failures.load(Ordering::Relaxed)
}
pub fn signature_failures(&self) -> u64 {
self.signature_failures.load(Ordering::Relaxed)
}
pub fn verification_failures(&self) -> u64 {
self.verification_failures.load(Ordering::Relaxed)
}
pub fn reset(&self) {
self.total_events.store(0, Ordering::Relaxed);
self.decision_events.store(0, Ordering::Relaxed);
self.config_change_events.store(0, Ordering::Relaxed);
self.ban_operation_events.store(0, Ordering::Relaxed);
self.system_events.store(0, Ordering::Relaxed);
self.error_events.store(0, Ordering::Relaxed);
self.batch_writes.store(0, Ordering::Relaxed);
self.write_failures.store(0, Ordering::Relaxed);
self.signature_failures.store(0, Ordering::Relaxed);
self.verification_failures.store(0, Ordering::Relaxed);
}
}
#[cfg(feature = "audit-log")]
fn sanitize_identifier(identifier: &str) -> String {
if identifier.contains('.') && identifier.parse::<std::net::IpAddr>().is_ok() {
let parts: Vec<&str> = identifier.split('.').collect();
if parts.len() == 4 {
return format!("{}.{}.xxx.xxx", parts[0], parts[1]);
}
}
if identifier.contains('@') {
let parts: Vec<&str> = identifier.split('@').collect();
if parts.len() == 2 {
let username = parts[0];
let masked_username = if username.len() > 3 {
format!("{}***", &username[..3])
} else {
"***".to_string()
};
return format!("{}@{}", masked_username, parts[1]);
}
}
if identifier.len() > 10 {
return format!(
"{}***{}",
&identifier[..3],
&identifier[identifier.len() - 3..]
);
}
if identifier.len() > 6 {
format!(
"{}***{}",
&identifier[..3],
&identifier[identifier.len() - 3..]
)
} else {
"***".to_string()
}
}
#[cfg(feature = "audit-log")]
#[derive(Debug, Clone)]
pub struct AuditLogConfig {
pub channel_capacity: usize,
pub batch_size: usize,
pub batch_timeout: Duration,
pub enabled: bool,
pub output_path: Option<String>,
pub max_file_size: Option<u64>,
pub max_files: Option<usize>,
pub signing_key: Option<SecretString>,
pub verify_on_read: bool,
}
#[cfg(feature = "audit-log")]
impl Default for AuditLogConfig {
fn default() -> Self {
Self {
channel_capacity: 10000,
batch_size: 100,
batch_timeout: Duration::from_secs(5),
enabled: true,
output_path: None,
max_file_size: Some(100 * 1024 * 1024), max_files: Some(10), signing_key: None,
verify_on_read: true,
}
}
}
#[cfg(feature = "audit-log")]
impl AuditLogConfig {
pub fn new() -> Self {
Self::default()
}
pub fn channel_capacity(mut self, capacity: usize) -> Self {
self.channel_capacity = capacity;
self
}
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn batch_timeout(mut self, timeout: Duration) -> Self {
self.batch_timeout = timeout;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn output_path(mut self, path: String) -> Self {
self.output_path = Some(path);
self
}
pub fn signing_key(mut self, key: String) -> Self {
self.signing_key = Some(SecretString::new(key.into_boxed_str()));
self
}
pub fn verify_on_read(mut self, verify: bool) -> Self {
self.verify_on_read = verify;
self
}
}
#[cfg(feature = "audit-log")]
#[derive(Debug)]
pub struct AuditLogger {
sender: Sender<AuditEvent>,
stats: Arc<AuditLogStats>,
config: AuditLogConfig,
write_handle: tokio::task::JoinHandle<()>,
}
#[cfg(feature = "audit-log")]
impl AuditLogger {
pub async fn new(config: AuditLogConfig) -> Self {
info!(
"创建审计日志记录器: enabled={}, signing_enabled={}",
config.enabled,
config.signing_key.is_some()
);
let (sender, receiver) = mpsc::channel(config.channel_capacity);
let stats = Arc::new(AuditLogStats::default());
let write_handle = tokio::spawn(Self::write_task(
receiver,
Arc::clone(&stats),
config.clone(),
));
Self {
sender,
stats,
config,
write_handle,
}
}
pub async fn default() -> Self {
Self::new(AuditLogConfig::default()).await
}
async fn write_task(
mut receiver: mpsc::Receiver<AuditEvent>,
stats: Arc<AuditLogStats>,
config: AuditLogConfig,
) {
let mut batch = Vec::with_capacity(config.batch_size);
let mut timeout = tokio::time::interval(config.batch_timeout);
loop {
tokio::select! {
result = receiver.recv() => {
match result {
Some(event) => {
batch.push(event);
stats.total_events.fetch_add(1, Ordering::Relaxed);
match batch.last().unwrap() {
AuditEvent::Decision { .. } => {
stats.decision_events.fetch_add(1, Ordering::Relaxed);
}
AuditEvent::ConfigChange { .. } => {
stats.config_change_events.fetch_add(1, Ordering::Relaxed);
}
AuditEvent::BanOperation { .. } => {
stats.ban_operation_events.fetch_add(1, Ordering::Relaxed);
}
AuditEvent::SystemEvent { .. } => {
stats.system_events.fetch_add(1, Ordering::Relaxed);
}
AuditEvent::ErrorEvent { .. } => {
stats.error_events.fetch_add(1, Ordering::Relaxed);
}
}
if batch.len() >= config.batch_size {
Self::write_batch(&batch, &config, &stats);
batch.clear();
}
}
None => {
if !batch.is_empty() {
Self::write_batch(&batch, &config, &stats);
}
break;
}
}
}
_ = timeout.tick() => {
if !batch.is_empty() {
Self::write_batch(&batch, &config, &stats);
batch.clear();
}
}
}
}
info!("审计日志写入任务结束");
}
fn write_batch(batch: &[AuditEvent], config: &AuditLogConfig, stats: &AuditLogStats) {
stats.batch_writes.fetch_add(1, Ordering::Relaxed);
for event in batch {
let entry = if let Some(ref signing_key) = config.signing_key {
let key = signing_key.expose_secret();
AuditLogEntry::with_signature(event.clone(), key)
} else {
AuditLogEntry::new(event.clone())
};
match serde_json::to_string_pretty(&entry) {
Ok(json) => {
info!("审计日志: {}", json);
if let Some(ref path) = config.output_path {
if let Err(e) = Self::write_to_file(path, &json, config) {
stats.write_failures.fetch_add(1, Ordering::Relaxed);
error!("写入审计日志文件失败: {}: {}", path, e);
} else {
trace!("成功写入审计日志文件: {}", path);
}
}
}
Err(e) => {
stats.write_failures.fetch_add(1, Ordering::Relaxed);
error!("序列化审计日志失败: {}", e);
}
}
}
}
fn write_to_file(path: &str, content: &str, config: &AuditLogConfig) -> std::io::Result<()> {
use std::fs::OpenOptions;
use std::io::Write;
if let Some(max_size) = config.max_file_size {
if let Ok(metadata) = std::fs::metadata(path) {
if metadata.len() >= max_size {
Self::rotate_log_file(path, config.max_files)?;
}
}
}
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
writeln!(file, "{}", content)?;
Ok(())
}
fn rotate_log_file(path: &str, max_files: Option<usize>) -> std::io::Result<()> {
let path_obj = std::path::Path::new(path);
let parent = path_obj.parent().unwrap_or(std::path::Path::new("."));
let stem = path_obj
.file_stem()
.unwrap_or(std::ffi::OsStr::new("audit"));
let extension = path_obj
.extension()
.and_then(|e| e.to_str())
.unwrap_or("log");
if let Some(max_files) = max_files {
let old_file = parent.join(format!(
"{}.{}.{}",
stem.to_str().unwrap(),
max_files,
extension
));
if old_file.exists() {
std::fs::remove_file(&old_file)?;
}
for i in (1..max_files).rev() {
let old_name =
parent.join(format!("{}.{}.{}", stem.to_str().unwrap(), i, extension));
let new_name = parent.join(format!(
"{}.{}.{}",
stem.to_str().unwrap(),
i + 1,
extension
));
if old_name.exists() {
std::fs::rename(&old_name, &new_name)?;
}
}
}
let rotated_name = parent.join(format!("{}.1.{}", stem.to_str().unwrap(), extension));
std::fs::rename(path, &rotated_name)?;
Ok(())
}
pub fn read_and_verify(
path: &str,
config: &AuditLogConfig,
stats: &AuditLogStats,
) -> Result<Vec<AuditLogEntry>, LimiteronError> {
use std::fs::File;
use std::io::{BufRead, BufReader};
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut entries = Vec::new();
for line in reader.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<AuditLogEntry>(line) {
Ok(entry) => {
if config.verify_on_read {
if let Some(ref signing_key) = config.signing_key {
let key = signing_key.expose_secret();
match entry.verify(key) {
Ok(true) => {
entries.push(entry);
}
Ok(false) => {
entries.push(entry);
}
Err(e) => {
stats.verification_failures.fetch_add(1, Ordering::Relaxed);
warn!("审计日志签名验证失败: {}", e);
entries.push(entry);
}
}
} else {
entries.push(entry);
}
} else {
entries.push(entry);
}
}
Err(e) => {
stats.verification_failures.fetch_add(1, Ordering::Relaxed);
warn!("解析审计日志条目失败: {}", e);
}
}
}
Ok(entries)
}
pub fn verify_integrity(&self, entry: &AuditLogEntry) -> Result<bool, LimiteronError> {
if let Some(ref signing_key) = self.config.signing_key {
let key = signing_key.expose_secret();
entry.verify(key)
} else {
Ok(false)
}
}
pub async fn log_decision(
&self,
identifier: String,
decision: String,
reason: String,
request_id: Option<String>,
) {
if !self.config.enabled {
return;
}
let sanitized_identifier = sanitize_identifier(&identifier);
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: sanitized_identifier,
decision,
reason,
request_id,
};
if let Err(e) = self.sender.send(event).await {
error!("发送决策事件失败: {}", e);
self.stats.write_failures.fetch_add(1, Ordering::Relaxed);
}
}
pub async fn log_config_change(
&self,
old_version: String,
new_version: String,
changes: Vec<String>,
operator: Option<String>,
) {
if !self.config.enabled {
return;
}
let event = AuditEvent::ConfigChange {
timestamp: Utc::now(),
old_version,
new_version,
changes,
operator,
};
if let Err(e) = self.sender.send(event).await {
error!("发送配置变更事件失败: {}", e);
self.stats.write_failures.fetch_add(1, Ordering::Relaxed);
}
}
pub async fn log_ban_operation(
&self,
target: String,
action: String,
reason: String,
operator: String,
expires_at: Option<DateTime<Utc>>,
) {
if !self.config.enabled {
return;
}
let event = AuditEvent::BanOperation {
timestamp: Utc::now(),
target,
action,
reason,
operator,
expires_at,
};
if let Err(e) = self.sender.send(event).await {
error!("发送封禁操作事件失败: {}", e);
self.stats.write_failures.fetch_add(1, Ordering::Relaxed);
}
}
pub async fn log_system_event(&self, level: String, name: String, details: String) {
if !self.config.enabled {
return;
}
let event = AuditEvent::SystemEvent {
timestamp: Utc::now(),
level,
name,
details,
};
if let Err(e) = self.sender.send(event).await {
error!("发送系统事件失败: {}", e);
self.stats.write_failures.fetch_add(1, Ordering::Relaxed);
}
}
pub async fn log_error_event(
&self,
error_type: String,
message: String,
stack_trace: Option<String>,
) {
if !self.config.enabled {
return;
}
let event = AuditEvent::ErrorEvent {
timestamp: Utc::now(),
error_type,
message,
stack_trace,
};
if let Err(e) = self.sender.send(event).await {
error!("发送错误事件失败: {}", e);
self.stats.write_failures.fetch_add(1, Ordering::Relaxed);
}
}
pub fn stats(&self) -> &AuditLogStats {
&self.stats
}
pub fn config(&self) -> &AuditLogConfig {
&self.config
}
pub async fn shutdown(mut self) {
info!("停止审计日志记录器");
let handle = std::mem::replace(&mut self.write_handle, tokio::spawn(async {}));
let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;
}
}
#[cfg(feature = "audit-log")]
impl Drop for AuditLogger {
fn drop(&mut self) {
self.write_handle.abort();
}
}
#[cfg(feature = "audit-log")]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_log_config_default() {
let config = AuditLogConfig::default();
assert_eq!(config.channel_capacity, 10000);
assert_eq!(config.batch_size, 100);
assert_eq!(config.batch_timeout, Duration::from_secs(5));
assert!(config.enabled);
assert!(config.signing_key.is_none());
assert!(config.verify_on_read);
}
#[test]
fn test_audit_log_config_builder() {
let config = AuditLogConfig::new()
.channel_capacity(5000)
.batch_size(50)
.batch_timeout(Duration::from_secs(10))
.enabled(false)
.output_path("/tmp/audit.log".to_string())
.signing_key("test-secret-key-32-bytes-long!".to_string())
.verify_on_read(false);
assert_eq!(config.channel_capacity, 5000);
assert_eq!(config.batch_size, 50);
assert_eq!(config.batch_timeout, Duration::from_secs(10));
assert!(!config.enabled);
assert_eq!(config.output_path, Some("/tmp/audit.log".to_string()));
assert!(config.signing_key.is_some());
assert!(!config.verify_on_read);
}
#[test]
fn test_audit_event_timestamp() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test".to_string(),
decision: "allowed".to_string(),
reason: "test".to_string(),
request_id: None,
};
assert!(event.timestamp() <= Utc::now());
}
#[test]
fn test_audit_event_operation() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test".to_string(),
decision: "allowed".to_string(),
reason: "test".to_string(),
request_id: None,
};
assert_eq!(event.operation(), "decision");
let event = AuditEvent::ConfigChange {
timestamp: Utc::now(),
old_version: "1.0".to_string(),
new_version: "2.0".to_string(),
changes: vec!["change1".to_string()],
operator: None,
};
assert_eq!(event.operation(), "config_change");
}
#[test]
fn test_audit_event_target() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "user123".to_string(),
decision: "allowed".to_string(),
reason: "test".to_string(),
request_id: None,
};
assert_eq!(event.target(), "user123");
let event = AuditEvent::ConfigChange {
timestamp: Utc::now(),
old_version: "1.0".to_string(),
new_version: "2.0".to_string(),
changes: vec![],
operator: None,
};
assert_eq!(event.target(), "1.0->2.0");
}
#[test]
fn test_audit_log_entry_signature() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let signing_key = "test-secret-key-32-bytes-long!";
let entry = AuditLogEntry::with_signature(event.clone(), signing_key);
assert!(entry.signature.is_some());
assert_eq!(entry.signature_version, Some(1));
let result = entry.verify(signing_key);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_audit_log_entry_signature_verification_failure() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let signing_key = "test-secret-key-32-bytes-long!";
let wrong_key = "wrong-secret-key-32-bytes-long!";
let entry = AuditLogEntry::with_signature(event, signing_key);
let result = entry.verify(wrong_key);
assert!(result.is_err());
}
#[test]
fn test_audit_log_entry_missing_signature() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let entry = AuditLogEntry::new(event);
let signing_key = "test-secret-key-32-bytes-long!";
let result = entry.verify(signing_key);
assert!(result.is_err());
assert!(matches!(result, Err(LimiteronError::AuditLogError(_))));
}
#[test]
fn test_constant_time_compare() {
assert!(AuditLogEntry::constant_time_compare("abc123", "abc123"));
assert!(!AuditLogEntry::constant_time_compare("abc123", "abc124"));
assert!(!AuditLogEntry::constant_time_compare("abc", "abcd"));
}
#[test]
fn test_signature_tampering_detection() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let signing_key = "test-secret-key-32-bytes-long!";
let mut entry = AuditLogEntry::with_signature(event.clone(), signing_key);
entry.signature = Some("tampered_signature".to_string());
let result = entry.verify(signing_key);
assert!(result.is_err());
}
#[test]
fn test_signature_deterministic() {
let timestamp = Utc::now();
let event = AuditEvent::Decision {
timestamp,
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let signing_key = "test-secret-key-32-bytes-long!";
let entry1 = AuditLogEntry::with_signature(event.clone(), signing_key);
let entry2 = AuditLogEntry::with_signature(event, signing_key);
assert_eq!(entry1.signature, entry2.signature);
}
#[test]
fn test_signature_serialization() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "test-user".to_string(),
decision: "allowed".to_string(),
reason: "within limit".to_string(),
request_id: None,
};
let signing_key = "test-secret-key-32-bytes-long!";
let entry = AuditLogEntry::with_signature(event, signing_key);
let json = serde_json::to_string(&entry).unwrap();
let deserialized: AuditLogEntry = serde_json::from_str(&json).unwrap();
let result = deserialized.verify(signing_key);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_audit_logger_new() {
let config = AuditLogConfig::default();
let logger = AuditLogger::new(config).await;
assert_eq!(logger.stats().total_events(), 0);
}
#[tokio::test]
async fn test_audit_logger_log_decision() {
let config = AuditLogConfig::default();
let logger = AuditLogger::new(config).await;
logger
.log_decision(
"user123".to_string(),
"allowed".to_string(),
"within limit".to_string(),
Some("req-123".to_string()),
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(logger.stats().decision_events(), 1);
}
#[tokio::test]
async fn test_audit_logger_with_signing() {
let config = AuditLogConfig::new()
.signing_key("test-secret-key-32-bytes-long!".to_string())
.verify_on_read(true);
let logger = AuditLogger::new(config).await;
logger
.log_decision(
"user123".to_string(),
"allowed".to_string(),
"within limit".to_string(),
None,
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(logger.stats().decision_events(), 1);
assert!(logger.config().signing_key.is_some());
}
#[test]
fn test_sanitize_identifier_ip() {
let result = sanitize_identifier("192.168.1.100");
assert_eq!(result, "192.168.xxx.xxx");
}
#[test]
fn test_sanitize_identifier_email() {
let result = sanitize_identifier("test@example.com");
assert_eq!(result, "tes***@example.com");
}
#[test]
fn test_sanitize_identifier_user_id() {
let result = sanitize_identifier("1234567890abcdef");
assert_eq!(result, "123***def");
}
#[test]
fn test_audit_log_stats() {
let stats = AuditLogStats::default();
stats.total_events.store(100, Ordering::Relaxed);
stats.decision_events.store(50, Ordering::Relaxed);
stats.signature_failures.store(2, Ordering::Relaxed);
assert_eq!(stats.total_events(), 100);
assert_eq!(stats.decision_events(), 50);
assert_eq!(stats.signature_failures(), 2);
}
#[test]
fn test_audit_event_all_variants_comprehensive() {
let now = Utc::now();
let decision = AuditEvent::Decision {
timestamp: now,
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let config_change = AuditEvent::ConfigChange {
timestamp: now,
old_version: "v1".into(),
new_version: "v2".into(),
changes: vec!["a".into()],
operator: None,
};
let ban_op = AuditEvent::BanOperation {
timestamp: now,
target: "ip".into(),
action: "ban".into(),
reason: "abuse".into(),
operator: "admin".into(),
expires_at: None,
};
let sys_event = AuditEvent::SystemEvent {
timestamp: now,
level: "info".into(),
name: "boot".into(),
details: "ok".into(),
};
let err_event = AuditEvent::ErrorEvent {
timestamp: now,
error_type: "e500".into(),
message: "fail".into(),
stack_trace: None,
};
assert_eq!(decision.timestamp(), now);
assert_eq!(config_change.timestamp(), now);
assert_eq!(ban_op.timestamp(), now);
assert_eq!(sys_event.timestamp(), now);
assert_eq!(err_event.timestamp(), now);
assert_eq!(decision.operation(), "decision");
assert_eq!(config_change.operation(), "config_change");
assert_eq!(ban_op.operation(), "ban_operation");
assert_eq!(sys_event.operation(), "system_event");
assert_eq!(err_event.operation(), "error_event");
assert_eq!(decision.target(), "usr");
assert_eq!(config_change.target(), "v1->v2");
assert_eq!(ban_op.target(), "ip");
assert_eq!(sys_event.target(), "boot");
assert_eq!(err_event.target(), "e500");
assert_eq!(decision.result(), "allow");
assert_eq!(config_change.result(), "a");
assert_eq!(ban_op.result(), "ban");
assert_eq!(sys_event.result(), "info");
assert_eq!(err_event.result(), "fail");
}
#[test]
fn test_audit_log_entry_new_and_sign_standalone() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let mut entry = AuditLogEntry::new(event);
assert!(entry.signature.is_none());
assert!(entry.signature_version.is_none());
entry.sign("test-key");
assert!(entry.signature.is_some());
assert_eq!(entry.signature_version, Some(1));
}
#[test]
fn test_constant_time_compare_edge_cases() {
assert!(AuditLogEntry::constant_time_compare("", ""));
assert!(!AuditLogEntry::constant_time_compare("", "a"));
assert!(!AuditLogEntry::constant_time_compare("a", ""));
assert!(!AuditLogEntry::constant_time_compare("hello", "world"));
assert!(!AuditLogEntry::constant_time_compare("abc", "ABC"));
assert!(AuditLogEntry::constant_time_compare(
"abc123xyz",
"abc123xyz"
));
}
#[test]
fn test_sanitize_identifier_edge_cases() {
assert_eq!(sanitize_identifier("ab@x.com"), "***@x.com");
assert_eq!(sanitize_identifier("abc@x.com"), "***@x.com");
assert_eq!(sanitize_identifier("abc"), "***");
assert_eq!(sanitize_identifier("ab"), "***");
assert_eq!(sanitize_identifier("abcdefg"), "abc***efg");
assert_eq!(sanitize_identifier("abcdefgh"), "abc***fgh");
assert_eq!(sanitize_identifier("abcdefghij"), "abc***hij");
assert_eq!(sanitize_identifier("abcdefghijklmnop"), "abc***nop");
}
#[test]
fn test_audit_log_stats_all_counters() {
let stats = AuditLogStats::default();
stats.total_events.store(10, Ordering::Relaxed);
stats.decision_events.store(3, Ordering::Relaxed);
stats.config_change_events.store(2, Ordering::Relaxed);
stats.ban_operation_events.store(1, Ordering::Relaxed);
stats.system_events.store(2, Ordering::Relaxed);
stats.error_events.store(2, Ordering::Relaxed);
stats.batch_writes.store(5, Ordering::Relaxed);
stats.write_failures.store(1, Ordering::Relaxed);
stats.signature_failures.store(3, Ordering::Relaxed);
stats.verification_failures.store(4, Ordering::Relaxed);
assert_eq!(stats.total_events(), 10);
assert_eq!(stats.decision_events(), 3);
assert_eq!(stats.config_change_events(), 2);
assert_eq!(stats.ban_operation_events(), 1);
assert_eq!(stats.system_events(), 2);
assert_eq!(stats.error_events(), 2);
assert_eq!(stats.batch_writes(), 5);
assert_eq!(stats.write_failures(), 1);
assert_eq!(stats.signature_failures(), 3);
assert_eq!(stats.verification_failures(), 4);
}
#[test]
fn test_audit_log_stats_reset() {
let stats = AuditLogStats::default();
stats.total_events.store(10, Ordering::Relaxed);
stats.decision_events.store(3, Ordering::Relaxed);
stats.verification_failures.store(4, Ordering::Relaxed);
assert_eq!(stats.total_events(), 10);
stats.reset();
assert_eq!(stats.total_events(), 0);
assert_eq!(stats.decision_events(), 0);
assert_eq!(stats.verification_failures(), 0);
}
#[tokio::test]
async fn test_audit_logger_default_and_accessors() {
let logger = AuditLogger::default().await;
assert_eq!(logger.stats().total_events(), 0);
assert!(logger.config().enabled);
}
#[tokio::test]
async fn test_audit_logger_shutdown() {
let config = AuditLogConfig::new();
let logger = AuditLogger::new(config).await;
logger.shutdown().await;
}
#[tokio::test]
async fn test_audit_logger_log_all_event_types() {
let config = AuditLogConfig::new().batch_size(10);
let logger = AuditLogger::new(config).await;
logger
.log_config_change(
"v1".into(),
"v2".into(),
vec!["change1".into()],
Some("admin".into()),
)
.await;
logger
.log_ban_operation(
"192.168.1.1".into(),
"ban".into(),
"abuse".into(),
"admin".into(),
None,
)
.await;
logger
.log_system_event("warn".into(), "disk".into(), "low space".into())
.await;
logger
.log_error_event(
"timeout".into(),
"connection failed".into(),
Some("trace".into()),
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(logger.stats().total_events(), 4);
assert_eq!(logger.stats().config_change_events(), 1);
assert_eq!(logger.stats().ban_operation_events(), 1);
assert_eq!(logger.stats().system_events(), 1);
assert_eq!(logger.stats().error_events(), 1);
}
#[tokio::test]
async fn test_audit_logger_disabled() {
let config = AuditLogConfig::new().enabled(false);
let logger = AuditLogger::new(config).await;
logger
.log_decision("usr".into(), "allow".into(), "test".into(), None)
.await;
logger
.log_config_change("v1".into(), "v2".into(), vec![], None)
.await;
logger
.log_ban_operation("ip".into(), "ban".into(), "test".into(), "op".into(), None)
.await;
logger
.log_system_event("info".into(), "test".into(), "test".into())
.await;
logger
.log_error_event("err".into(), "test".into(), None)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(logger.stats().total_events(), 0);
assert_eq!(logger.stats().decision_events(), 0);
assert_eq!(logger.stats().config_change_events(), 0);
assert_eq!(logger.stats().ban_operation_events(), 0);
assert_eq!(logger.stats().system_events(), 0);
assert_eq!(logger.stats().error_events(), 0);
}
#[tokio::test]
async fn test_audit_logger_verify_integrity() {
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let key = "test-key-32-bytes-long-secret!!";
let config = AuditLogConfig::new().signing_key(key.into());
let logger = AuditLogger::new(config).await;
let entry = AuditLogEntry::with_signature(event.clone(), key);
assert!(logger.verify_integrity(&entry).unwrap());
let config2 = AuditLogConfig::new();
let logger2 = AuditLogger::new(config2).await;
let entry2 = AuditLogEntry::new(event);
assert!(!logger2.verify_integrity(&entry2).unwrap());
}
#[test]
fn test_read_and_verify_basic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit.log");
let p = path.to_str().unwrap();
let event1 = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let event2 = AuditEvent::SystemEvent {
timestamp: Utc::now(),
level: "info".into(),
name: "boot".into(),
details: "ok".into(),
};
let entry1 = AuditLogEntry::new(event1);
let entry2 = AuditLogEntry::new(event2);
let json1 = serde_json::to_string(&entry1).unwrap();
let json2 = serde_json::to_string(&entry2).unwrap();
{
let mut f = std::fs::File::create(&path).unwrap();
use std::io::Write;
writeln!(f, "{}", json1).unwrap();
writeln!(f).unwrap();
writeln!(f, "{}", json2).unwrap();
writeln!(f, "{{corrupted}}").unwrap();
}
let config = AuditLogConfig::new().verify_on_read(false);
let stats = AuditLogStats::default();
let entries = AuditLogger::read_and_verify(p, &config, &stats).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(stats.verification_failures(), 1);
}
#[test]
fn test_read_and_verify_with_signature() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit_sig.log");
let p = path.to_str().unwrap();
let key = "test-key-sig-32-bytes-long!!!!";
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let entry = AuditLogEntry::with_signature(event, key);
let json = serde_json::to_string(&entry).unwrap();
std::fs::write(p, format!("{}\n", json)).unwrap();
let config = AuditLogConfig::new()
.verify_on_read(true)
.signing_key(key.into());
let stats = AuditLogStats::default();
let entries = AuditLogger::read_and_verify(p, &config, &stats).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(stats.verification_failures(), 0);
let config2 = AuditLogConfig::new()
.verify_on_read(true)
.signing_key("wrong-key-32-bytes-long!!!!!".into());
let stats2 = AuditLogStats::default();
let entries2 = AuditLogger::read_and_verify(p, &config2, &stats2).unwrap();
assert_eq!(entries2.len(), 1);
assert_eq!(stats2.verification_failures(), 1);
}
#[test]
fn test_read_and_verify_no_signing_key() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit_nokey.log");
let p = path.to_str().unwrap();
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
let entry = AuditLogEntry::with_signature(event, "some-key");
let json = serde_json::to_string(&entry).unwrap();
std::fs::write(p, format!("{}\n", json)).unwrap();
let config = AuditLogConfig::new().verify_on_read(true);
let stats = AuditLogStats::default();
let entries = AuditLogger::read_and_verify(p, &config, &stats).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(stats.verification_failures(), 0);
}
#[test]
fn test_write_to_file_direct() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test_direct.log");
let p = path.to_str().unwrap().to_string();
let config = AuditLogConfig::new().output_path(p.clone());
let result = AuditLogger::write_to_file(&p, "line1", &config);
assert!(result.is_ok());
let content = std::fs::read_to_string(&p).unwrap();
assert_eq!(content, "line1\n");
let result2 = AuditLogger::write_to_file(&p, "line2", &config);
assert!(result2.is_ok());
let content2 = std::fs::read_to_string(&p).unwrap();
assert_eq!(content2, "line1\nline2\n");
}
#[tokio::test]
async fn test_audit_logger_write_to_file_integration() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit_integration.log");
let p = path.to_str().unwrap().to_string();
let config = AuditLogConfig::new().batch_size(1).output_path(p.clone());
let logger = AuditLogger::new(config).await;
logger
.log_decision(
"user123".into(),
"allow".into(),
"within_limit".into(),
None,
)
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
let content = std::fs::read_to_string(&p).unwrap();
assert!(content.contains("allow"));
assert_eq!(logger.stats().batch_writes(), 1);
}
#[test]
fn test_rotate_log_file_basic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit_rot.log");
let p = path.to_str().unwrap();
std::fs::write(p, "content").unwrap();
let result = AuditLogger::rotate_log_file(p, Some(3));
assert!(result.is_ok());
let rotated = dir.path().join("audit_rot.1.log");
assert!(rotated.exists());
let content = std::fs::read_to_string(&rotated).unwrap();
assert_eq!(content, "content");
}
#[test]
fn test_write_batch_file_write_failure() {
let dir = tempfile::tempdir().unwrap();
let blocker = dir.path().join("blocker_file");
std::fs::write(&blocker, "blocker").unwrap();
let invalid_path = blocker.join("audit.log").to_str().unwrap().to_string();
let config = AuditLogConfig::new().output_path(invalid_path);
let stats = AuditLogStats::default();
let event = AuditEvent::Decision {
timestamp: Utc::now(),
identifier: "usr".into(),
decision: "allow".into(),
reason: "test".into(),
request_id: None,
};
AuditLogger::write_batch(&[event], &config, &stats);
assert_eq!(stats.write_failures(), 1);
assert_eq!(stats.batch_writes(), 1);
}
#[test]
fn test_write_to_file_triggers_full_rotation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("audit.log");
let p = path.to_str().unwrap().to_string();
std::fs::write(&path, "0".repeat(100)).unwrap();
std::fs::write(dir.path().join("audit.1.log"), "old1").unwrap();
std::fs::write(dir.path().join("audit.2.log"), "old2").unwrap();
std::fs::write(dir.path().join("audit.3.log"), "old3").unwrap();
let mut config = AuditLogConfig::new().output_path(p.clone());
config.max_file_size = Some(10);
config.max_files = Some(3);
let result = AuditLogger::write_to_file(&p, "new entry", &config);
assert!(result.is_ok());
let rotated = dir.path().join("audit.1.log");
assert!(rotated.exists());
let content = std::fs::read_to_string(&rotated).unwrap();
assert_eq!(content, "0".repeat(100));
let a2 = dir.path().join("audit.2.log");
assert_eq!(std::fs::read_to_string(&a2).unwrap(), "old1");
let a3 = dir.path().join("audit.3.log");
assert_eq!(std::fs::read_to_string(&a3).unwrap(), "old2");
let new_content = std::fs::read_to_string(&path).unwrap();
assert_eq!(new_content, "new entry\n");
}
}