use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum EventSeverity {
Debug,
#[default]
Info,
Warning,
Error,
Critical,
}
impl std::fmt::Display for EventSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventSeverity::Debug => write!(f, "DEBUG"),
EventSeverity::Info => write!(f, "INFO"),
EventSeverity::Warning => write!(f, "WARNING"),
EventSeverity::Error => write!(f, "ERROR"),
EventSeverity::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EventType {
LoginSuccess,
LoginFailed,
Logout,
PasswordChanged,
PasswordResetRequested,
PasswordResetCompleted,
MfaEnabled,
MfaDisabled,
MfaVerified,
MfaFailed,
AccountLocked,
AccountUnlocked,
AccountCreated,
AccountDeleted,
PermissionChanged,
RoleChanged,
ApiKeyCreated,
ApiKeyRevoked,
ApiKeyUsed,
OauthClientCreated,
OauthClientDeleted,
OauthClientSecretRotated,
OauthTokenIssued,
OauthTokenRevoked,
OauthTokenIntrospectionFailed,
WebauthnRegistration,
WebauthnRegistrationFailed,
WebauthnAssertion,
WebauthnAssertionFailed,
MagicLinkIssued,
MagicLinkUsed,
MagicLinkExpired,
OtpSent,
OtpVerified,
OtpFailed,
RefreshTokenIssued,
RefreshTokenRotated,
RefreshTokenReuseDetected,
SessionCreated,
SessionExpired,
SessionRevoked,
SuspiciousActivity,
RateLimitTriggered,
IpBanned,
Custom(String),
}
impl std::fmt::Display for EventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventType::LoginSuccess => write!(f, "login_success"),
EventType::LoginFailed => write!(f, "login_failed"),
EventType::Logout => write!(f, "logout"),
EventType::PasswordChanged => write!(f, "password_changed"),
EventType::PasswordResetRequested => write!(f, "password_reset_requested"),
EventType::PasswordResetCompleted => write!(f, "password_reset_completed"),
EventType::MfaEnabled => write!(f, "mfa_enabled"),
EventType::MfaDisabled => write!(f, "mfa_disabled"),
EventType::MfaVerified => write!(f, "mfa_verified"),
EventType::MfaFailed => write!(f, "mfa_failed"),
EventType::AccountLocked => write!(f, "account_locked"),
EventType::AccountUnlocked => write!(f, "account_unlocked"),
EventType::AccountCreated => write!(f, "account_created"),
EventType::AccountDeleted => write!(f, "account_deleted"),
EventType::PermissionChanged => write!(f, "permission_changed"),
EventType::RoleChanged => write!(f, "role_changed"),
EventType::ApiKeyCreated => write!(f, "api_key_created"),
EventType::ApiKeyRevoked => write!(f, "api_key_revoked"),
EventType::ApiKeyUsed => write!(f, "api_key_used"),
EventType::OauthClientCreated => write!(f, "oauth_client_created"),
EventType::OauthClientDeleted => write!(f, "oauth_client_deleted"),
EventType::OauthClientSecretRotated => write!(f, "oauth_client_secret_rotated"),
EventType::OauthTokenIssued => write!(f, "oauth_token_issued"),
EventType::OauthTokenRevoked => write!(f, "oauth_token_revoked"),
EventType::OauthTokenIntrospectionFailed => {
write!(f, "oauth_token_introspection_failed")
}
EventType::WebauthnRegistration => write!(f, "webauthn_registration"),
EventType::WebauthnRegistrationFailed => {
write!(f, "webauthn_registration_failed")
}
EventType::WebauthnAssertion => write!(f, "webauthn_assertion"),
EventType::WebauthnAssertionFailed => write!(f, "webauthn_assertion_failed"),
EventType::MagicLinkIssued => write!(f, "magic_link_issued"),
EventType::MagicLinkUsed => write!(f, "magic_link_used"),
EventType::MagicLinkExpired => write!(f, "magic_link_expired"),
EventType::OtpSent => write!(f, "otp_sent"),
EventType::OtpVerified => write!(f, "otp_verified"),
EventType::OtpFailed => write!(f, "otp_failed"),
EventType::RefreshTokenIssued => write!(f, "refresh_token_issued"),
EventType::RefreshTokenRotated => write!(f, "refresh_token_rotated"),
EventType::RefreshTokenReuseDetected => write!(f, "refresh_token_reuse_detected"),
EventType::SessionCreated => write!(f, "session_created"),
EventType::SessionExpired => write!(f, "session_expired"),
EventType::SessionRevoked => write!(f, "session_revoked"),
EventType::SuspiciousActivity => write!(f, "suspicious_activity"),
EventType::RateLimitTriggered => write!(f, "rate_limit_triggered"),
EventType::IpBanned => write!(f, "ip_banned"),
EventType::Custom(name) => write!(f, "custom:{}", name),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityEvent {
pub id: String,
pub event_type: EventType,
pub severity: EventSeverity,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub details: HashMap<String, String>,
pub timestamp: DateTime<Utc>,
}
impl SecurityEvent {
pub fn new(event_type: EventType, severity: EventSeverity) -> Self {
Self {
id: generate_event_id(),
event_type,
severity,
user_id: None,
ip_address: None,
user_agent: None,
message: None,
details: HashMap::new(),
timestamp: Utc::now(),
}
}
pub fn custom(name: impl Into<String>, severity: EventSeverity) -> Self {
Self::new(EventType::Custom(name.into()), severity)
}
pub fn login_success(user_id: impl Into<String>, ip: impl Into<String>) -> Self {
Self::new(EventType::LoginSuccess, EventSeverity::Info)
.with_user_id(user_id)
.with_ip(ip)
.with_message("User logged in successfully")
}
pub fn login_failed(user_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(EventType::LoginFailed, EventSeverity::Warning)
.with_user_id(user_id)
.with_message(reason)
}
pub fn logout(user_id: impl Into<String>) -> Self {
Self::new(EventType::Logout, EventSeverity::Info)
.with_user_id(user_id)
.with_message("User logged out")
}
pub fn password_changed(user_id: impl Into<String>) -> Self {
Self::new(EventType::PasswordChanged, EventSeverity::Info)
.with_user_id(user_id)
.with_message("Password changed successfully")
}
pub fn password_reset_requested(user_id: impl Into<String>) -> Self {
Self::new(EventType::PasswordResetRequested, EventSeverity::Info)
.with_user_id(user_id)
.with_message("Password reset requested")
}
pub fn mfa_enabled(user_id: impl Into<String>) -> Self {
Self::new(EventType::MfaEnabled, EventSeverity::Info)
.with_user_id(user_id)
.with_message("MFA enabled for account")
}
pub fn mfa_disabled(user_id: impl Into<String>) -> Self {
Self::new(EventType::MfaDisabled, EventSeverity::Warning)
.with_user_id(user_id)
.with_message("MFA disabled for account")
}
pub fn mfa_failed(user_id: impl Into<String>) -> Self {
Self::new(EventType::MfaFailed, EventSeverity::Warning)
.with_user_id(user_id)
.with_message("MFA verification failed")
}
pub fn account_locked(user_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(EventType::AccountLocked, EventSeverity::Warning)
.with_user_id(user_id)
.with_message(format!("Account locked: {}", reason.into()))
}
pub fn account_unlocked(user_id: impl Into<String>) -> Self {
Self::new(EventType::AccountUnlocked, EventSeverity::Info)
.with_user_id(user_id)
.with_message("Account unlocked")
}
pub fn suspicious_activity(details: impl Into<String>) -> Self {
Self::new(EventType::SuspiciousActivity, EventSeverity::Critical).with_message(details)
}
pub fn rate_limit_triggered(
identifier: impl Into<String>,
ip: Option<impl Into<String>>,
) -> Self {
let mut event = Self::new(EventType::RateLimitTriggered, EventSeverity::Warning)
.with_detail("identifier", identifier.into());
if let Some(ip) = ip {
event = event.with_ip(ip);
}
event
}
pub fn ip_banned(ip: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(EventType::IpBanned, EventSeverity::Warning)
.with_ip(ip)
.with_message(reason)
}
pub fn oauth_client_created(client_id: impl Into<String>) -> Self {
Self::new(EventType::OauthClientCreated, EventSeverity::Info)
.with_detail("client_id", client_id.into())
.with_message("OAuth client created")
}
pub fn oauth_client_deleted(client_id: impl Into<String>) -> Self {
Self::new(EventType::OauthClientDeleted, EventSeverity::Warning)
.with_detail("client_id", client_id.into())
.with_message("OAuth client deleted")
}
pub fn oauth_client_secret_rotated(client_id: impl Into<String>) -> Self {
Self::new(EventType::OauthClientSecretRotated, EventSeverity::Info)
.with_detail("client_id", client_id.into())
.with_message("OAuth client secret rotated")
}
pub fn oauth_token_issued(
client_id: impl Into<String>,
user_id: Option<impl Into<String>>,
) -> Self {
let mut event = Self::new(EventType::OauthTokenIssued, EventSeverity::Info)
.with_detail("client_id", client_id.into())
.with_message("OAuth token issued");
if let Some(user_id) = user_id {
event = event.with_user_id(user_id);
}
event
}
pub fn oauth_token_revoked(client_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(EventType::OauthTokenRevoked, EventSeverity::Warning)
.with_detail("client_id", client_id.into())
.with_message(format!("OAuth token revoked: {}", reason.into()))
}
pub fn oauth_token_introspection_failed(
client_id: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::new(EventType::OauthTokenIntrospectionFailed, EventSeverity::Warning)
.with_detail("client_id", client_id.into())
.with_message(format!("OAuth token introspection failed: {}", reason.into()))
}
pub fn webauthn_registration(user_id: impl Into<String>, rp_id: impl Into<String>) -> Self {
Self::new(EventType::WebauthnRegistration, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("rp_id", rp_id.into())
.with_message("WebAuthn registration successful")
}
pub fn webauthn_registration_failed(
user_id: impl Into<String>,
rp_id: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::new(EventType::WebauthnRegistrationFailed, EventSeverity::Warning)
.with_user_id(user_id)
.with_detail("rp_id", rp_id.into())
.with_message(format!("WebAuthn registration failed: {}", reason.into()))
}
pub fn webauthn_assertion(user_id: impl Into<String>, rp_id: impl Into<String>) -> Self {
Self::new(EventType::WebauthnAssertion, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("rp_id", rp_id.into())
.with_message("WebAuthn assertion successful")
}
pub fn webauthn_assertion_failed(
user_id: impl Into<String>,
rp_id: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::new(EventType::WebauthnAssertionFailed, EventSeverity::Warning)
.with_user_id(user_id)
.with_detail("rp_id", rp_id.into())
.with_message(format!("WebAuthn assertion failed: {}", reason.into()))
}
pub fn magic_link_issued(recipient: impl Into<String>) -> Self {
Self::new(EventType::MagicLinkIssued, EventSeverity::Info)
.with_detail("recipient", recipient.into())
.with_message("Magic link issued")
}
pub fn magic_link_used(recipient: impl Into<String>) -> Self {
Self::new(EventType::MagicLinkUsed, EventSeverity::Info)
.with_detail("recipient", recipient.into())
.with_message("Magic link used")
}
pub fn magic_link_expired(recipient: impl Into<String>) -> Self {
Self::new(EventType::MagicLinkExpired, EventSeverity::Warning)
.with_detail("recipient", recipient.into())
.with_message("Magic link expired")
}
pub fn otp_sent(recipient: impl Into<String>) -> Self {
Self::new(EventType::OtpSent, EventSeverity::Info)
.with_detail("recipient", recipient.into())
.with_message("OTP sent")
}
pub fn otp_verified(recipient: impl Into<String>) -> Self {
Self::new(EventType::OtpVerified, EventSeverity::Info)
.with_detail("recipient", recipient.into())
.with_message("OTP verified")
}
pub fn otp_failed(recipient: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(EventType::OtpFailed, EventSeverity::Warning)
.with_detail("recipient", recipient.into())
.with_message(format!("OTP verification failed: {}", reason.into()))
}
pub fn refresh_token_issued(user_id: impl Into<String>, family_id: impl Into<String>) -> Self {
Self::new(EventType::RefreshTokenIssued, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("family_id", family_id.into())
.with_message("Refresh token issued")
}
pub fn refresh_token_rotated(
user_id: impl Into<String>,
family_id: impl Into<String>,
) -> Self {
Self::new(EventType::RefreshTokenRotated, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("family_id", family_id.into())
.with_message("Refresh token rotated")
}
pub fn refresh_token_reuse_detected(
user_id: impl Into<String>,
family_id: impl Into<String>,
) -> Self {
Self::new(EventType::RefreshTokenReuseDetected, EventSeverity::Critical)
.with_user_id(user_id)
.with_detail("family_id", family_id.into())
.with_message("Refresh token reuse detected")
}
pub fn api_key_created(user_id: impl Into<String>, key_id: impl Into<String>) -> Self {
Self::new(EventType::ApiKeyCreated, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("key_id", key_id.into())
.with_message("API key created")
}
pub fn api_key_revoked(user_id: impl Into<String>, key_id: impl Into<String>) -> Self {
Self::new(EventType::ApiKeyRevoked, EventSeverity::Info)
.with_user_id(user_id)
.with_detail("key_id", key_id.into())
.with_message("API key revoked")
}
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn with_ip(mut self, ip: impl Into<String>) -> Self {
self.ip_address = Some(ip.into());
self
}
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.details.insert(key.into(), value.into());
self
}
pub fn with_severity(mut self, severity: EventSeverity) -> Self {
self.severity = severity;
self
}
pub fn event_name(&self) -> String {
self.event_type.to_string()
}
pub fn is_high_severity(&self) -> bool {
matches!(
self.severity,
EventSeverity::Error | EventSeverity::Critical
)
}
pub fn is_auth_event(&self) -> bool {
matches!(
self.event_type,
EventType::LoginSuccess
| EventType::LoginFailed
| EventType::Logout
| EventType::MfaVerified
| EventType::MfaFailed
| EventType::MagicLinkIssued
| EventType::MagicLinkUsed
| EventType::MagicLinkExpired
| EventType::OtpSent
| EventType::OtpVerified
| EventType::OtpFailed
| EventType::WebauthnRegistration
| EventType::WebauthnRegistrationFailed
| EventType::WebauthnAssertion
| EventType::WebauthnAssertionFailed
)
}
}
fn generate_event_id() -> String {
use crate::random::generate_random_hex;
format!(
"evt_{}",
generate_random_hex(16).unwrap_or_else(|_| "unknown".to_string())
)
}
pub trait AuditLogger: Send + Sync {
fn log(&self, event: SecurityEvent);
fn log_batch(&self, events: Vec<SecurityEvent>) {
for event in events {
self.log(event);
}
}
}
#[derive(Debug, Default)]
pub struct InMemoryAuditLogger {
events: Arc<RwLock<Vec<SecurityEvent>>>,
max_events: Option<usize>,
}
impl InMemoryAuditLogger {
pub fn new() -> Self {
Self {
events: Arc::new(RwLock::new(Vec::new())),
max_events: None,
}
}
pub fn with_max_events(max: usize) -> Self {
Self {
events: Arc::new(RwLock::new(Vec::new())),
max_events: Some(max),
}
}
pub fn get_events(&self) -> Vec<SecurityEvent> {
self.events.read().unwrap().clone()
}
pub fn event_count(&self) -> usize {
self.events.read().unwrap().len()
}
pub fn get_events_by_user(&self, user_id: &str) -> Vec<SecurityEvent> {
self.events
.read()
.unwrap()
.iter()
.filter(|e| e.user_id.as_deref() == Some(user_id))
.cloned()
.collect()
}
pub fn get_events_by_type(&self, event_type: &EventType) -> Vec<SecurityEvent> {
self.events
.read()
.unwrap()
.iter()
.filter(|e| &e.event_type == event_type)
.cloned()
.collect()
}
pub fn get_events_by_severity(&self, severity: EventSeverity) -> Vec<SecurityEvent> {
self.events
.read()
.unwrap()
.iter()
.filter(|e| e.severity == severity)
.cloned()
.collect()
}
pub fn get_events_in_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Vec<SecurityEvent> {
self.events
.read()
.unwrap()
.iter()
.filter(|e| e.timestamp >= start && e.timestamp <= end)
.cloned()
.collect()
}
pub fn get_recent_events(&self, count: usize) -> Vec<SecurityEvent> {
let events = self.events.read().unwrap();
events.iter().rev().take(count).cloned().collect()
}
pub fn clear(&self) {
self.events.write().unwrap().clear();
}
pub fn get_high_severity_events(&self) -> Vec<SecurityEvent> {
self.events
.read()
.unwrap()
.iter()
.filter(|e| e.is_high_severity())
.cloned()
.collect()
}
pub fn get_stats(&self) -> AuditStats {
let events = self.events.read().unwrap();
let mut stats = AuditStats {
total_events: events.len(),
..Default::default()
};
for event in events.iter() {
match event.severity {
EventSeverity::Debug => stats.debug_count += 1,
EventSeverity::Info => stats.info_count += 1,
EventSeverity::Warning => stats.warning_count += 1,
EventSeverity::Error => stats.error_count += 1,
EventSeverity::Critical => stats.critical_count += 1,
}
*stats.events_by_type.entry(event.event_name()).or_insert(0) += 1;
}
stats
}
}
impl AuditLogger for InMemoryAuditLogger {
fn log(&self, event: SecurityEvent) {
let mut events = self.events.write().unwrap();
if let Some(max) = self.max_events {
while events.len() >= max {
events.remove(0);
}
}
events.push(event);
}
}
impl Clone for InMemoryAuditLogger {
fn clone(&self) -> Self {
Self {
events: Arc::clone(&self.events),
max_events: self.max_events,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct AuditStats {
pub total_events: usize,
pub debug_count: usize,
pub info_count: usize,
pub warning_count: usize,
pub error_count: usize,
pub critical_count: usize,
pub events_by_type: HashMap<String, usize>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoOpAuditLogger;
impl NoOpAuditLogger {
pub fn new() -> Self {
Self
}
}
impl AuditLogger for NoOpAuditLogger {
fn log(&self, _event: SecurityEvent) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_event_creation() {
let event = SecurityEvent::login_success("user123", "192.168.1.1");
assert_eq!(event.event_type, EventType::LoginSuccess);
assert_eq!(event.severity, EventSeverity::Info);
assert_eq!(event.user_id, Some("user123".to_string()));
assert_eq!(event.ip_address, Some("192.168.1.1".to_string()));
assert!(event.id.starts_with("evt_"));
}
#[test]
fn test_security_event_builder() {
let event = SecurityEvent::custom("custom_event", EventSeverity::Warning)
.with_user_id("user456")
.with_ip("10.0.0.1")
.with_user_agent("Mozilla/5.0")
.with_detail("key1", "value1")
.with_detail("key2", "value2");
assert_eq!(event.user_id, Some("user456".to_string()));
assert_eq!(event.ip_address, Some("10.0.0.1".to_string()));
assert_eq!(event.user_agent, Some("Mozilla/5.0".to_string()));
assert_eq!(event.details.get("key1"), Some(&"value1".to_string()));
assert_eq!(event.details.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_in_memory_logger() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_failed("user2", "Bad password"));
logger.log(SecurityEvent::password_changed("user1"));
assert_eq!(logger.event_count(), 3);
let events = logger.get_events();
assert_eq!(events.len(), 3);
}
#[test]
fn test_filter_by_user() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_failed("user2", "Bad password"));
logger.log(SecurityEvent::password_changed("user1"));
let user1_events = logger.get_events_by_user("user1");
assert_eq!(user1_events.len(), 2);
let user2_events = logger.get_events_by_user("user2");
assert_eq!(user2_events.len(), 1);
}
#[test]
fn test_filter_by_type() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_failed("user2", "Bad password"));
logger.log(SecurityEvent::login_success("user3", "10.0.0.1"));
let login_success = logger.get_events_by_type(&EventType::LoginSuccess);
assert_eq!(login_success.len(), 2);
let login_failed = logger.get_events_by_type(&EventType::LoginFailed);
assert_eq!(login_failed.len(), 1);
}
#[test]
fn test_filter_by_severity() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_failed("user2", "Bad password"));
logger.log(SecurityEvent::suspicious_activity("Unusual login pattern"));
let info_events = logger.get_events_by_severity(EventSeverity::Info);
assert_eq!(info_events.len(), 1);
let warning_events = logger.get_events_by_severity(EventSeverity::Warning);
assert_eq!(warning_events.len(), 1);
let critical_events = logger.get_events_by_severity(EventSeverity::Critical);
assert_eq!(critical_events.len(), 1);
}
#[test]
fn test_max_events_limit() {
let logger = InMemoryAuditLogger::with_max_events(3);
logger.log(SecurityEvent::login_success("user1", "1.1.1.1"));
logger.log(SecurityEvent::login_success("user2", "2.2.2.2"));
logger.log(SecurityEvent::login_success("user3", "3.3.3.3"));
logger.log(SecurityEvent::login_success("user4", "4.4.4.4"));
assert_eq!(logger.event_count(), 3);
let events = logger.get_events();
assert!(
events
.iter()
.all(|e| e.user_id != Some("user1".to_string()))
);
}
#[test]
fn test_clear_events() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_success("user2", "192.168.1.2"));
assert_eq!(logger.event_count(), 2);
logger.clear();
assert_eq!(logger.event_count(), 0);
}
#[test]
fn test_get_stats() {
let logger = InMemoryAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
logger.log(SecurityEvent::login_failed("user2", "Bad password"));
logger.log(SecurityEvent::suspicious_activity("Test"));
let stats = logger.get_stats();
assert_eq!(stats.total_events, 3);
assert_eq!(stats.info_count, 1);
assert_eq!(stats.warning_count, 1);
assert_eq!(stats.critical_count, 1);
}
#[test]
fn test_is_high_severity() {
let info_event = SecurityEvent::login_success("user1", "192.168.1.1");
let critical_event = SecurityEvent::suspicious_activity("Test");
assert!(!info_event.is_high_severity());
assert!(critical_event.is_high_severity());
}
#[test]
fn test_is_auth_event() {
let login = SecurityEvent::login_success("user1", "192.168.1.1");
let password_change = SecurityEvent::password_changed("user1");
assert!(login.is_auth_event());
assert!(!password_change.is_auth_event());
}
#[test]
fn test_event_serialization() {
let event =
SecurityEvent::login_success("user123", "192.168.1.1").with_detail("browser", "Chrome");
let json = serde_json::to_string(&event).unwrap();
let deserialized: SecurityEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_id, event.user_id);
assert_eq!(deserialized.ip_address, event.ip_address);
assert_eq!(
deserialized.details.get("browser"),
Some(&"Chrome".to_string())
);
}
#[test]
fn test_noop_logger() {
let logger = NoOpAuditLogger::new();
logger.log(SecurityEvent::login_success("user1", "192.168.1.1"));
}
#[test]
fn test_batch_logging() {
let logger = InMemoryAuditLogger::new();
let events = vec![
SecurityEvent::login_success("user1", "1.1.1.1"),
SecurityEvent::login_success("user2", "2.2.2.2"),
SecurityEvent::login_success("user3", "3.3.3.3"),
];
logger.log_batch(events);
assert_eq!(logger.event_count(), 3);
}
#[test]
fn test_clone_logger_shares_state() {
let logger1 = InMemoryAuditLogger::new();
let logger2 = logger1.clone();
logger1.log(SecurityEvent::login_success("user1", "192.168.1.1"));
assert_eq!(logger2.event_count(), 1);
}
#[test]
fn test_refresh_token_reuse_detected_event() {
let event =
SecurityEvent::refresh_token_reuse_detected("user1", "fam123");
assert_eq!(event.event_type, EventType::RefreshTokenReuseDetected);
assert_eq!(event.severity, EventSeverity::Critical);
assert_eq!(event.user_id.as_deref(), Some("user1"));
assert_eq!(event.details.get("family_id"), Some(&"fam123".to_string()));
}
#[test]
fn test_oauth_token_issued_event() {
let event = SecurityEvent::oauth_token_issued("client-1", Some("user-9"));
assert_eq!(event.event_type, EventType::OauthTokenIssued);
assert_eq!(event.severity, EventSeverity::Info);
assert_eq!(event.user_id.as_deref(), Some("user-9"));
assert_eq!(event.details.get("client_id"), Some(&"client-1".to_string()));
}
}