use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub kind: AuditEventKind,
pub severity: AuditSeverity,
pub source: AuditSource,
pub method: Option<String>,
pub path: Option<String>,
pub status_code: Option<u16>,
pub duration_ms: Option<u64>,
pub service_name: String,
pub metadata: Option<serde_json::Value>,
pub hash: Option<String>,
pub previous_hash: Option<String>,
pub sequence: u64,
}
impl AuditEvent {
pub fn new(kind: AuditEventKind, severity: AuditSeverity, service_name: String) -> Self {
Self {
id: Uuid::new_v4(),
timestamp: Utc::now(),
kind,
severity,
source: AuditSource::default(),
method: None,
path: None,
status_code: None,
duration_ms: None,
service_name,
metadata: None,
hash: None,
previous_hash: None,
sequence: 0,
}
}
pub fn with_source(mut self, source: AuditSource) -> Self {
self.source = source;
self
}
pub fn with_http(
mut self,
method: String,
path: String,
status_code: Option<u16>,
duration_ms: Option<u64>,
) -> Self {
self.method = Some(method);
self.path = Some(path);
self.status_code = status_code;
self.duration_ms = duration_ms;
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuditEventKind {
AuthLoginSuccess,
AuthLoginFailed,
AuthLogout,
AuthTokenRefresh,
AuthTokenRevoked,
AuthPasswordChanged,
AuthApiKeyCreated,
AuthApiKeyRevoked,
AuthOAuthCallback,
AuthPermissionDenied,
#[cfg(feature = "login-lockout")]
AuthAccountLocked,
#[cfg(feature = "login-lockout")]
AuthAccountUnlocked,
#[cfg(feature = "accounts")]
AccountCreated,
#[cfg(feature = "accounts")]
AccountDisabled,
#[cfg(feature = "accounts")]
AccountEnabled,
#[cfg(feature = "accounts")]
AccountLocked,
#[cfg(feature = "accounts")]
AccountUnlocked,
#[cfg(feature = "accounts")]
AccountExpired,
#[cfg(feature = "accounts")]
AccountDeleted,
#[cfg(feature = "accounts")]
AccountUpdated,
AuthKeyRotated,
AuthKeyRetired,
AuthKeyRotationFailed,
ConfigLoaded,
ConfigDriftDetected,
HttpRequest,
HttpRequestDenied,
Custom(String),
}
impl std::fmt::Display for AuditEventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AuthLoginSuccess => write!(f, "auth.login.success"),
Self::AuthLoginFailed => write!(f, "auth.login.failed"),
Self::AuthLogout => write!(f, "auth.logout"),
Self::AuthTokenRefresh => write!(f, "auth.token.refresh"),
Self::AuthTokenRevoked => write!(f, "auth.token.revoked"),
Self::AuthPasswordChanged => write!(f, "auth.password.changed"),
Self::AuthApiKeyCreated => write!(f, "auth.apikey.created"),
Self::AuthApiKeyRevoked => write!(f, "auth.apikey.revoked"),
Self::AuthOAuthCallback => write!(f, "auth.oauth.callback"),
Self::AuthPermissionDenied => write!(f, "auth.permission.denied"),
#[cfg(feature = "login-lockout")]
Self::AuthAccountLocked => write!(f, "auth.account.locked"),
#[cfg(feature = "login-lockout")]
Self::AuthAccountUnlocked => write!(f, "auth.account.unlocked"),
#[cfg(feature = "accounts")]
Self::AccountCreated => write!(f, "account.created"),
#[cfg(feature = "accounts")]
Self::AccountDisabled => write!(f, "account.disabled"),
#[cfg(feature = "accounts")]
Self::AccountEnabled => write!(f, "account.enabled"),
#[cfg(feature = "accounts")]
Self::AccountLocked => write!(f, "account.locked"),
#[cfg(feature = "accounts")]
Self::AccountUnlocked => write!(f, "account.unlocked"),
#[cfg(feature = "accounts")]
Self::AccountExpired => write!(f, "account.expired"),
#[cfg(feature = "accounts")]
Self::AccountDeleted => write!(f, "account.deleted"),
#[cfg(feature = "accounts")]
Self::AccountUpdated => write!(f, "account.updated"),
Self::AuthKeyRotated => write!(f, "auth.key.rotated"),
Self::AuthKeyRetired => write!(f, "auth.key.retired"),
Self::AuthKeyRotationFailed => write!(f, "auth.key.rotation_failed"),
Self::ConfigLoaded => write!(f, "config.loaded"),
Self::ConfigDriftDetected => write!(f, "config.drift_detected"),
Self::HttpRequest => write!(f, "http.request"),
Self::HttpRequestDenied => write!(f, "http.request.denied"),
Self::Custom(name) => write!(f, "custom.{}", name),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AuditSeverity {
Emergency = 0,
Alert = 1,
Critical = 2,
Error = 3,
Warning = 4,
Notice = 5,
Informational = 6,
Debug = 7,
}
impl AuditSeverity {
pub fn as_syslog_severity(&self) -> u8 {
*self as u8
}
}
impl std::fmt::Display for AuditSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Emergency => write!(f, "EMERGENCY"),
Self::Alert => write!(f, "ALERT"),
Self::Critical => write!(f, "CRITICAL"),
Self::Error => write!(f, "ERROR"),
Self::Warning => write!(f, "WARNING"),
Self::Notice => write!(f, "NOTICE"),
Self::Informational => write!(f, "INFO"),
Self::Debug => write!(f, "DEBUG"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditSource {
pub ip: Option<String>,
pub user_agent: Option<String>,
pub subject: Option<String>,
pub request_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_event_new() {
let event = AuditEvent::new(
AuditEventKind::AuthLoginSuccess,
AuditSeverity::Informational,
"test-service".to_string(),
);
assert_eq!(event.kind, AuditEventKind::AuthLoginSuccess);
assert_eq!(event.service_name, "test-service");
assert!(event.hash.is_none());
assert_eq!(event.sequence, 0);
}
#[test]
fn test_audit_event_with_http() {
let event = AuditEvent::new(
AuditEventKind::HttpRequest,
AuditSeverity::Informational,
"test-service".to_string(),
)
.with_http("GET".into(), "/api/v1/users".into(), Some(200), Some(42));
assert_eq!(event.method, Some("GET".to_string()));
assert_eq!(event.path, Some("/api/v1/users".to_string()));
assert_eq!(event.status_code, Some(200));
assert_eq!(event.duration_ms, Some(42));
}
#[test]
fn test_audit_event_kind_display() {
assert_eq!(
AuditEventKind::AuthLoginSuccess.to_string(),
"auth.login.success"
);
assert_eq!(AuditEventKind::HttpRequest.to_string(), "http.request");
assert_eq!(
AuditEventKind::Custom("user.delete".to_string()).to_string(),
"custom.user.delete"
);
}
#[test]
fn test_audit_severity_syslog_value() {
assert_eq!(AuditSeverity::Emergency.as_syslog_severity(), 0);
assert_eq!(AuditSeverity::Alert.as_syslog_severity(), 1);
assert_eq!(AuditSeverity::Informational.as_syslog_severity(), 6);
assert_eq!(AuditSeverity::Debug.as_syslog_severity(), 7);
}
#[test]
fn test_audit_event_serde_roundtrip() {
let event = AuditEvent::new(
AuditEventKind::AuthLoginFailed,
AuditSeverity::Warning,
"test".to_string(),
)
.with_source(AuditSource {
ip: Some("192.168.1.1".to_string()),
user_agent: Some("curl/8.0".to_string()),
subject: None,
request_id: Some("req-123".to_string()),
});
let json = serde_json::to_string(&event).unwrap();
let deserialized: AuditEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, event.id);
assert_eq!(deserialized.kind, AuditEventKind::AuthLoginFailed);
assert_eq!(deserialized.source.ip, Some("192.168.1.1".to_string()));
}
}