use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub user_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub action: AuditAction,
pub resource_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_id: Option<String>,
pub result: AuditResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditAction {
Login,
Logout,
Create,
Read,
Update,
Delete,
Execute,
Access,
ConfigChange,
PermissionChange,
ToolCall,
LlmRequest,
SessionOperation,
Other(String),
}
impl AuditAction {
pub fn as_str(&self) -> &str {
match self {
AuditAction::Login => "login",
AuditAction::Logout => "logout",
AuditAction::Create => "create",
AuditAction::Read => "read",
AuditAction::Update => "update",
AuditAction::Delete => "delete",
AuditAction::Execute => "execute",
AuditAction::Access => "access",
AuditAction::ConfigChange => "config_change",
AuditAction::PermissionChange => "permission_change",
AuditAction::ToolCall => "tool_call",
AuditAction::LlmRequest => "llm_request",
AuditAction::SessionOperation => "session_operation",
AuditAction::Other(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditResult {
Success,
Failure {
error_code: String,
error_message: String,
},
Denied {
reason: String,
},
}
impl AuditResult {
pub fn is_success(&self) -> bool {
matches!(self, AuditResult::Success)
}
pub fn success() -> Self {
AuditResult::Success
}
pub fn failure(code: &str, message: &str) -> Self {
AuditResult::Failure {
error_code: code.to_string(),
error_message: message.to_string(),
}
}
pub fn denied(reason: &str) -> Self {
AuditResult::Denied {
reason: reason.to_string(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct AuditFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<AuditAction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<AuditResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
impl AuditEntry {
pub fn new(user_id: &str, action: AuditAction, resource_type: &str) -> Self {
Self {
id: Uuid::new_v4(),
timestamp: Utc::now(),
user_id: user_id.to_string(),
session_id: None,
action,
resource_type: resource_type.to_string(),
resource_id: None,
result: AuditResult::Success,
details: None,
ip_address: None,
request_id: None,
duration_ms: None,
}
}
pub fn with_session(mut self, session_id: &str) -> Self {
self.session_id = Some(session_id.to_string());
self
}
pub fn with_resource_id(mut self, resource_id: &str) -> Self {
self.resource_id = Some(resource_id.to_string());
self
}
pub fn with_result(mut self, result: AuditResult) -> Self {
self.result = result;
self
}
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
pub fn with_ip(mut self, ip_address: &str) -> Self {
self.ip_address = Some(ip_address.to_string());
self
}
pub fn with_request_id(mut self, request_id: &str) -> Self {
self.request_id = Some(request_id.to_string());
self
}
pub fn with_duration(mut self, duration_ms: u64) -> Self {
self.duration_ms = Some(duration_ms);
self
}
pub fn matches_filter(&self, filter: &AuditFilter) -> bool {
if let Some(user_id) = &filter.user_id {
if self.user_id != *user_id {
return false;
}
}
if let Some(action) = &filter.action {
if self.action.as_str() != action.as_str() {
return false;
}
}
if let Some(resource_type) = &filter.resource_type {
if self.resource_type != *resource_type {
return false;
}
}
if let Some(start_time) = &filter.start_time {
if self.timestamp < *start_time {
return false;
}
}
if let Some(end_time) = &filter.end_time {
if self.timestamp > *end_time {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub enum ExportFormat {
Json,
Csv,
Syslog,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_entry_creation() {
let entry = AuditEntry::new("user123", AuditAction::Read, "document");
assert_eq!(entry.user_id, "user123");
assert!(entry.result.is_success());
}
#[test]
fn test_audit_entry_with_details() {
let entry = AuditEntry::new("user123", AuditAction::Execute, "tool")
.with_details(serde_json::json!({ "tool_name": "test" }));
assert!(entry.details.is_some());
}
#[test]
fn test_audit_result() {
let success = AuditResult::success();
assert!(success.is_success());
let failure = AuditResult::failure("E001", "Test error");
assert!(!failure.is_success());
}
#[test]
fn test_audit_filter_matching() {
let entry = AuditEntry::new("user123", AuditAction::Read, "document");
let filter = AuditFilter {
user_id: Some("user123".to_string()),
..Default::default()
};
assert!(entry.matches_filter(&filter));
let filter2 = AuditFilter {
user_id: Some("other_user".to_string()),
..Default::default()
};
assert!(!entry.matches_filter(&filter2));
}
#[test]
fn test_serialize_audit_entry() {
let entry = AuditEntry::new("user123", AuditAction::Login, "session");
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("user123"));
assert!(json.contains("Login")); }
}