use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecurityEventType {
AuthenticationSuccess,
AuthenticationFailure,
Logout,
SessionCreated,
SessionDestroyed,
SessionExpired,
AccessGranted,
AccessDenied,
InsufficientPermissions,
AccountLocked,
AccountUnlocked,
PasswordChanged,
PasswordResetRequested,
TokenGenerated,
TokenRefreshed,
TokenRevoked,
TokenExpired,
InvalidToken,
RateLimitExceeded,
RateLimitWarning,
CsrfValidationFailed,
CsrfTokenMissing,
BruteForceDetected,
SuspiciousIp,
MultipleFailures,
Custom(String),
}
impl fmt::Display for SecurityEventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecurityEventType::AuthenticationSuccess => write!(f, "AUTHENTICATION_SUCCESS"),
SecurityEventType::AuthenticationFailure => write!(f, "AUTHENTICATION_FAILURE"),
SecurityEventType::Logout => write!(f, "LOGOUT"),
SecurityEventType::SessionCreated => write!(f, "SESSION_CREATED"),
SecurityEventType::SessionDestroyed => write!(f, "SESSION_DESTROYED"),
SecurityEventType::SessionExpired => write!(f, "SESSION_EXPIRED"),
SecurityEventType::AccessGranted => write!(f, "ACCESS_GRANTED"),
SecurityEventType::AccessDenied => write!(f, "ACCESS_DENIED"),
SecurityEventType::InsufficientPermissions => write!(f, "INSUFFICIENT_PERMISSIONS"),
SecurityEventType::AccountLocked => write!(f, "ACCOUNT_LOCKED"),
SecurityEventType::AccountUnlocked => write!(f, "ACCOUNT_UNLOCKED"),
SecurityEventType::PasswordChanged => write!(f, "PASSWORD_CHANGED"),
SecurityEventType::PasswordResetRequested => write!(f, "PASSWORD_RESET_REQUESTED"),
SecurityEventType::TokenGenerated => write!(f, "TOKEN_GENERATED"),
SecurityEventType::TokenRefreshed => write!(f, "TOKEN_REFRESHED"),
SecurityEventType::TokenRevoked => write!(f, "TOKEN_REVOKED"),
SecurityEventType::TokenExpired => write!(f, "TOKEN_EXPIRED"),
SecurityEventType::InvalidToken => write!(f, "INVALID_TOKEN"),
SecurityEventType::RateLimitExceeded => write!(f, "RATE_LIMIT_EXCEEDED"),
SecurityEventType::RateLimitWarning => write!(f, "RATE_LIMIT_WARNING"),
SecurityEventType::CsrfValidationFailed => write!(f, "CSRF_VALIDATION_FAILED"),
SecurityEventType::CsrfTokenMissing => write!(f, "CSRF_TOKEN_MISSING"),
SecurityEventType::BruteForceDetected => write!(f, "BRUTE_FORCE_DETECTED"),
SecurityEventType::SuspiciousIp => write!(f, "SUSPICIOUS_IP"),
SecurityEventType::MultipleFailures => write!(f, "MULTIPLE_FAILURES"),
SecurityEventType::Custom(name) => write!(f, "CUSTOM_{}", name.to_uppercase()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum SecurityEventSeverity {
#[default]
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for SecurityEventSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecurityEventSeverity::Info => write!(f, "INFO"),
SecurityEventSeverity::Warning => write!(f, "WARNING"),
SecurityEventSeverity::Error => write!(f, "ERROR"),
SecurityEventSeverity::Critical => write!(f, "CRITICAL"),
}
}
}
impl SecurityEventType {
pub fn default_severity(&self) -> SecurityEventSeverity {
match self {
SecurityEventType::AuthenticationSuccess
| SecurityEventType::Logout
| SecurityEventType::SessionCreated
| SecurityEventType::AccessGranted
| SecurityEventType::TokenGenerated
| SecurityEventType::TokenRefreshed
| SecurityEventType::PasswordChanged => SecurityEventSeverity::Info,
SecurityEventType::SessionExpired
| SecurityEventType::TokenExpired
| SecurityEventType::RateLimitWarning => SecurityEventSeverity::Warning,
SecurityEventType::AuthenticationFailure
| SecurityEventType::SessionDestroyed
| SecurityEventType::AccessDenied
| SecurityEventType::InsufficientPermissions
| SecurityEventType::TokenRevoked
| SecurityEventType::InvalidToken
| SecurityEventType::RateLimitExceeded
| SecurityEventType::CsrfValidationFailed
| SecurityEventType::CsrfTokenMissing
| SecurityEventType::AccountLocked
| SecurityEventType::PasswordResetRequested => SecurityEventSeverity::Error,
SecurityEventType::BruteForceDetected
| SecurityEventType::SuspiciousIp
| SecurityEventType::MultipleFailures => SecurityEventSeverity::Critical,
SecurityEventType::AccountUnlocked | SecurityEventType::Custom(_) => {
SecurityEventSeverity::Info
}
}
}
}
#[derive(Debug, Clone)]
pub struct SecurityEvent {
pub id: String,
pub timestamp: u64,
pub event_type: SecurityEventType,
pub severity: SecurityEventSeverity,
pub username: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub path: Option<String>,
pub method: Option<String>,
pub session_id: Option<String>,
pub details: HashMap<String, String>,
pub error: Option<String>,
}
impl SecurityEvent {
pub fn new(event_type: SecurityEventType) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self {
id: generate_event_id(),
timestamp: now,
severity: event_type.default_severity(),
event_type,
username: None,
ip_address: None,
user_agent: None,
path: None,
method: None,
session_id: None,
details: HashMap::new(),
error: None,
}
}
pub fn username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn ip_address(mut self, ip: impl Into<String>) -> Self {
self.ip_address = Some(ip.into());
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn method(mut self, method: impl Into<String>) -> Self {
self.method = Some(method.into());
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.session_id = Some(id.into());
self
}
pub fn severity(mut self, severity: SecurityEventSeverity) -> Self {
self.severity = severity;
self
}
pub fn detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.details.insert(key.into(), value.into());
self
}
pub fn error(mut self, error: impl Into<String>) -> Self {
self.error = Some(error.into());
self
}
pub fn login_success(username: &str, ip: &str) -> Self {
Self::new(SecurityEventType::AuthenticationSuccess)
.username(username)
.ip_address(ip)
}
pub fn login_failure(username: &str, ip: &str, reason: &str) -> Self {
Self::new(SecurityEventType::AuthenticationFailure)
.username(username)
.ip_address(ip)
.error(reason)
}
pub fn access_denied(username: &str, path: &str, ip: &str) -> Self {
Self::new(SecurityEventType::AccessDenied)
.username(username)
.path(path)
.ip_address(ip)
}
pub fn rate_limit_exceeded(ip: &str, path: &str) -> Self {
Self::new(SecurityEventType::RateLimitExceeded)
.ip_address(ip)
.path(path)
}
pub fn account_locked(username: &str, ip: &str, reason: &str) -> Self {
Self::new(SecurityEventType::AccountLocked)
.username(username)
.ip_address(ip)
.detail("reason", reason)
}
pub fn brute_force_detected(ip: &str, attempts: u32) -> Self {
Self::new(SecurityEventType::BruteForceDetected)
.ip_address(ip)
.detail("attempts", attempts.to_string())
}
pub fn to_log_line(&self) -> String {
let mut parts = vec![
format!("[{}]", self.severity),
format!("[{}]", self.event_type),
];
if let Some(ref username) = self.username {
parts.push(format!("user={}", username));
}
if let Some(ref ip) = self.ip_address {
parts.push(format!("ip={}", ip));
}
if let Some(ref path) = self.path {
parts.push(format!("path={}", path));
}
if let Some(ref error) = self.error {
parts.push(format!("error=\"{}\"", error));
}
for (k, v) in &self.details {
parts.push(format!("{}={}", k, v));
}
parts.join(" ")
}
#[cfg(any(feature = "jwt", feature = "session", feature = "oauth2"))]
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| self.to_log_line())
}
#[cfg(not(any(feature = "jwt", feature = "session", feature = "oauth2")))]
pub fn to_json(&self) -> String {
self.to_log_line()
}
}
#[cfg(any(feature = "jwt", feature = "session", feature = "oauth2"))]
impl serde::Serialize for SecurityEvent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("SecurityEvent", 12)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("timestamp", &self.timestamp)?;
state.serialize_field("event_type", &self.event_type.to_string())?;
state.serialize_field("severity", &self.severity.to_string())?;
state.serialize_field("username", &self.username)?;
state.serialize_field("ip_address", &self.ip_address)?;
state.serialize_field("user_agent", &self.user_agent)?;
state.serialize_field("path", &self.path)?;
state.serialize_field("method", &self.method)?;
state.serialize_field("session_id", &self.session_id)?;
state.serialize_field("details", &self.details)?;
state.serialize_field("error", &self.error)?;
state.end()
}
}
fn generate_event_id() -> String {
use rand::Rng;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_micros();
let random: u32 = rand::thread_rng().gen();
format!("{:x}-{:08x}", timestamp, random)
}
pub trait SecurityEventHandler: Send + Sync {
fn handle(&self, event: &SecurityEvent);
}
#[derive(Default)]
pub struct StdoutHandler {
min_severity: SecurityEventSeverity,
}
impl StdoutHandler {
pub fn new() -> Self {
Self::default()
}
pub fn min_severity(mut self, severity: SecurityEventSeverity) -> Self {
self.min_severity = severity;
self
}
}
impl SecurityEventHandler for StdoutHandler {
fn handle(&self, event: &SecurityEvent) {
if event.severity >= self.min_severity {
println!("[SECURITY] {}", event.to_log_line());
}
}
}
#[derive(Default, Clone, Copy)]
pub struct TracingHandler;
impl TracingHandler {
pub fn new() -> Self {
Self
}
}
impl SecurityEventHandler for TracingHandler {
fn handle(&self, event: &SecurityEvent) {
let username = event.username.as_deref().unwrap_or("-");
let ip = event.ip_address.as_deref().unwrap_or("-");
let path = event.path.as_deref().unwrap_or("-");
let method = event.method.as_deref().unwrap_or("-");
let event_type = event.event_type.to_string();
let error_msg = event.error.as_deref().unwrap_or("");
match event.severity {
SecurityEventSeverity::Info => {
tracing::info!(
target: "actix_security::audit",
event_type = %event_type,
user = %username,
ip = %ip,
path = %path,
method = %method,
"Security event"
);
}
SecurityEventSeverity::Warning => {
tracing::warn!(
target: "actix_security::audit",
event_type = %event_type,
user = %username,
ip = %ip,
path = %path,
method = %method,
"Security warning"
);
}
SecurityEventSeverity::Error => {
tracing::error!(
target: "actix_security::audit",
event_type = %event_type,
user = %username,
ip = %ip,
path = %path,
method = %method,
error = %error_msg,
"Security error"
);
}
SecurityEventSeverity::Critical => {
tracing::error!(
target: "actix_security::audit",
event_type = %event_type,
user = %username,
ip = %ip,
path = %path,
method = %method,
error = %error_msg,
severity = "CRITICAL",
"Critical security event"
);
}
}
}
}
pub struct ClosureHandler<F>
where
F: Fn(&SecurityEvent) + Send + Sync,
{
handler: F,
}
impl<F> ClosureHandler<F>
where
F: Fn(&SecurityEvent) + Send + Sync,
{
pub fn new(handler: F) -> Self {
Self { handler }
}
}
impl<F> SecurityEventHandler for ClosureHandler<F>
where
F: Fn(&SecurityEvent) + Send + Sync,
{
fn handle(&self, event: &SecurityEvent) {
(self.handler)(event);
}
}
#[derive(Clone)]
pub struct InMemoryEventStore {
events: Arc<RwLock<Vec<SecurityEvent>>>,
max_events: usize,
}
impl Default for InMemoryEventStore {
fn default() -> Self {
Self::new()
}
}
impl InMemoryEventStore {
pub fn new() -> Self {
Self {
events: Arc::new(RwLock::new(Vec::new())),
max_events: 10000,
}
}
pub fn max_events(mut self, max: usize) -> Self {
self.max_events = max;
self
}
pub async fn get_events(&self) -> Vec<SecurityEvent> {
self.events.read().await.clone()
}
pub async fn get_events_by_type(&self, event_type: &SecurityEventType) -> Vec<SecurityEvent> {
self.events
.read()
.await
.iter()
.filter(|e| &e.event_type == event_type)
.cloned()
.collect()
}
pub async fn get_events_by_user(&self, username: &str) -> Vec<SecurityEvent> {
self.events
.read()
.await
.iter()
.filter(|e| e.username.as_deref() == Some(username))
.cloned()
.collect()
}
pub async fn clear(&self) {
self.events.write().await.clear();
}
}
impl SecurityEventHandler for InMemoryEventStore {
fn handle(&self, event: &SecurityEvent) {
let rt = tokio::runtime::Handle::try_current();
if let Ok(handle) = rt {
let events = Arc::clone(&self.events);
let event = event.clone();
let max = self.max_events;
handle.spawn(async move {
let mut guard = events.write().await;
guard.push(event);
if guard.len() > max {
guard.remove(0);
}
});
}
}
}
#[derive(Clone)]
pub struct AuditLogger {
handlers: Arc<Vec<Arc<dyn SecurityEventHandler>>>,
enabled: bool,
}
impl Default for AuditLogger {
fn default() -> Self {
Self::new()
}
}
impl AuditLogger {
pub fn new() -> Self {
Self {
handlers: Arc::new(Vec::new()),
enabled: true,
}
}
pub fn with_stdout() -> Self {
Self::new().add_handler(StdoutHandler::new())
}
pub fn add_handler<H: SecurityEventHandler + 'static>(mut self, handler: H) -> Self {
let handlers = Arc::make_mut(&mut self.handlers);
handlers.push(Arc::new(handler));
self
}
pub fn with_handler<F>(self, handler: F) -> Self
where
F: Fn(&SecurityEvent) + Send + Sync + 'static,
{
self.add_handler(ClosureHandler::new(handler))
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn log(&self, event: SecurityEvent) {
if !self.enabled {
return;
}
for handler in self.handlers.iter() {
handler.handle(&event);
}
}
pub fn log_login_success(&self, username: &str, ip: &str) {
self.log(SecurityEvent::login_success(username, ip));
}
pub fn log_login_failure(&self, username: &str, ip: &str, reason: &str) {
self.log(SecurityEvent::login_failure(username, ip, reason));
}
pub fn log_access_denied(&self, username: &str, path: &str, ip: &str) {
self.log(SecurityEvent::access_denied(username, path, ip));
}
pub fn log_rate_limit_exceeded(&self, ip: &str, path: &str) {
self.log(SecurityEvent::rate_limit_exceeded(ip, path));
}
}
static GLOBAL_LOGGER: std::sync::OnceLock<AuditLogger> = std::sync::OnceLock::new();
pub fn init_global_logger(logger: AuditLogger) {
let _ = GLOBAL_LOGGER.set(logger);
}
pub fn global_logger() -> &'static AuditLogger {
GLOBAL_LOGGER.get_or_init(AuditLogger::new)
}
pub fn audit_log(event: SecurityEvent) {
global_logger().log(event);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_creation() {
let event = SecurityEvent::login_success("admin", "192.168.1.1");
assert_eq!(event.event_type, SecurityEventType::AuthenticationSuccess);
assert_eq!(event.username, Some("admin".to_string()));
assert_eq!(event.ip_address, Some("192.168.1.1".to_string()));
assert_eq!(event.severity, SecurityEventSeverity::Info);
}
#[test]
fn test_event_builder() {
let event = SecurityEvent::new(SecurityEventType::AccessDenied)
.username("user1")
.ip_address("10.0.0.1")
.path("/admin")
.detail("reason", "missing role")
.error("Access denied");
assert_eq!(event.username, Some("user1".to_string()));
assert_eq!(event.path, Some("/admin".to_string()));
assert!(event.details.contains_key("reason"));
assert_eq!(event.error, Some("Access denied".to_string()));
}
#[test]
fn test_severity_ordering() {
assert!(SecurityEventSeverity::Info < SecurityEventSeverity::Warning);
assert!(SecurityEventSeverity::Warning < SecurityEventSeverity::Error);
assert!(SecurityEventSeverity::Error < SecurityEventSeverity::Critical);
}
#[test]
fn test_event_type_display() {
assert_eq!(
SecurityEventType::AuthenticationSuccess.to_string(),
"AUTHENTICATION_SUCCESS"
);
assert_eq!(
SecurityEventType::Custom("test".to_string()).to_string(),
"CUSTOM_TEST"
);
}
#[test]
fn test_log_line_format() {
let event = SecurityEvent::login_failure("admin", "192.168.1.1", "Invalid password");
let log_line = event.to_log_line();
assert!(log_line.contains("[ERROR]"));
assert!(log_line.contains("[AUTHENTICATION_FAILURE]"));
assert!(log_line.contains("user=admin"));
assert!(log_line.contains("ip=192.168.1.1"));
}
#[test]
fn test_default_severity() {
assert_eq!(
SecurityEventType::AuthenticationSuccess.default_severity(),
SecurityEventSeverity::Info
);
assert_eq!(
SecurityEventType::BruteForceDetected.default_severity(),
SecurityEventSeverity::Critical
);
}
#[test]
fn test_audit_logger_with_closure() {
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let logger = AuditLogger::new().with_handler(move |_event| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
logger.log_login_success("admin", "127.0.0.1");
logger.log_login_failure("user", "127.0.0.1", "Bad password");
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[test]
fn test_disabled_logger() {
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let logger = AuditLogger::new()
.with_handler(move |_event| {
counter_clone.fetch_add(1, Ordering::SeqCst);
})
.enabled(false);
logger.log_login_success("admin", "127.0.0.1");
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
}