use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub event_type: EventType,
pub session_id: String,
pub user: String,
pub client_ip: Option<IpAddr>,
pub path: Option<PathBuf>,
pub dest_path: Option<PathBuf>,
pub bytes: Option<u64>,
pub result: EventResult,
pub details: Option<String>,
pub protocol: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
AuthSuccess,
AuthFailure,
AuthRateLimited,
SessionStart,
SessionEnd,
CommandExecuted,
CommandBlocked,
FileOpenRead,
FileOpenWrite,
FileRead,
FileWrite,
FileClose,
FileUploaded,
FileDownloaded,
FileDeleted,
FileRenamed,
DirectoryCreated,
DirectoryDeleted,
DirectoryListed,
TransferDenied,
TransferAllowed,
IpBlocked,
IpUnblocked,
SuspiciousActivity,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EventResult {
Success,
Failure,
Denied,
Error,
}
impl AuditEvent {
pub fn new(event_type: EventType, user: String, session_id: String) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
event_type,
session_id,
user,
client_ip: None,
path: None,
dest_path: None,
bytes: None,
result: EventResult::Success,
details: None,
protocol: None,
}
}
pub fn with_client_ip(mut self, ip: IpAddr) -> Self {
self.client_ip = Some(ip);
self
}
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn with_dest_path(mut self, dest_path: PathBuf) -> Self {
self.dest_path = Some(dest_path);
self
}
pub fn with_bytes(mut self, bytes: u64) -> Self {
self.bytes = Some(bytes);
self
}
pub fn with_result(mut self, result: EventResult) -> Self {
self.result = result;
self
}
pub fn with_details(mut self, details: String) -> Self {
self.details = Some(details);
self
}
pub fn with_protocol(mut self, protocol: &str) -> Self {
self.protocol = Some(protocol.to_string());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_event_creation() {
let event = AuditEvent::new(
EventType::AuthSuccess,
"alice".to_string(),
"session-123".to_string(),
);
assert_eq!(event.event_type, EventType::AuthSuccess);
assert_eq!(event.user, "alice");
assert_eq!(event.session_id, "session-123");
assert_eq!(event.result, EventResult::Success);
assert!(event.client_ip.is_none());
assert!(event.path.is_none());
assert!(!event.id.is_empty());
}
#[test]
fn test_audit_event_builder() {
let ip: IpAddr = "192.168.1.100".parse().unwrap();
let event = AuditEvent::new(
EventType::FileUploaded,
"bob".to_string(),
"session-456".to_string(),
)
.with_client_ip(ip)
.with_path(PathBuf::from("/home/bob/file.txt"))
.with_bytes(1024)
.with_result(EventResult::Success)
.with_protocol("sftp")
.with_details("Upload completed".to_string());
assert_eq!(event.client_ip, Some(ip));
assert_eq!(event.path, Some(PathBuf::from("/home/bob/file.txt")));
assert_eq!(event.bytes, Some(1024));
assert_eq!(event.result, EventResult::Success);
assert_eq!(event.protocol, Some("sftp".to_string()));
assert_eq!(event.details, Some("Upload completed".to_string()));
}
#[test]
fn test_event_type_serialization() {
let event_type = EventType::AuthSuccess;
let serialized = serde_json::to_string(&event_type).unwrap();
assert_eq!(serialized, r#""auth_success""#);
let deserialized: EventType = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, EventType::AuthSuccess);
}
#[test]
fn test_event_result_serialization() {
let result = EventResult::Denied;
let serialized = serde_json::to_string(&result).unwrap();
assert_eq!(serialized, r#""denied""#);
let deserialized: EventResult = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, EventResult::Denied);
}
#[test]
fn test_full_event_serialization() {
let event = AuditEvent::new(
EventType::SessionStart,
"charlie".to_string(),
"session-789".to_string(),
)
.with_client_ip("10.0.0.1".parse().unwrap())
.with_protocol("ssh");
let serialized = serde_json::to_string(&event).unwrap();
let deserialized: AuditEvent = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.event_type, event.event_type);
assert_eq!(deserialized.user, event.user);
assert_eq!(deserialized.session_id, event.session_id);
assert_eq!(deserialized.client_ip, event.client_ip);
assert_eq!(deserialized.protocol, event.protocol);
}
#[test]
fn test_event_with_dest_path() {
let event = AuditEvent::new(
EventType::FileRenamed,
"dave".to_string(),
"session-101".to_string(),
)
.with_path(PathBuf::from("/home/dave/old.txt"))
.with_dest_path(PathBuf::from("/home/dave/new.txt"));
assert_eq!(event.path, Some(PathBuf::from("/home/dave/old.txt")));
assert_eq!(event.dest_path, Some(PathBuf::from("/home/dave/new.txt")));
}
}