1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditEntry {
12 pub id: Uuid,
14 pub timestamp: DateTime<Utc>,
16 pub user_id: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub session_id: Option<String>,
21 pub action: AuditAction,
23 pub resource_type: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub resource_id: Option<String>,
28 pub result: AuditResult,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub details: Option<serde_json::Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub ip_address: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub request_id: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub duration_ms: Option<u64>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum AuditAction {
47 Login,
49 Logout,
51 Create,
53 Read,
55 Update,
57 Delete,
59 Execute,
61 Access,
63 ConfigChange,
65 PermissionChange,
67 ToolCall,
69 LlmRequest,
71 SessionOperation,
73 Other(String),
75}
76
77impl AuditAction {
78 pub fn as_str(&self) -> &str {
79 match self {
80 AuditAction::Login => "login",
81 AuditAction::Logout => "logout",
82 AuditAction::Create => "create",
83 AuditAction::Read => "read",
84 AuditAction::Update => "update",
85 AuditAction::Delete => "delete",
86 AuditAction::Execute => "execute",
87 AuditAction::Access => "access",
88 AuditAction::ConfigChange => "config_change",
89 AuditAction::PermissionChange => "permission_change",
90 AuditAction::ToolCall => "tool_call",
91 AuditAction::LlmRequest => "llm_request",
92 AuditAction::SessionOperation => "session_operation",
93 AuditAction::Other(s) => s.as_str(),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum AuditResult {
101 Success,
103 Failure {
105 error_code: String,
107 error_message: String,
109 },
110 Denied {
112 reason: String,
114 },
115}
116
117impl AuditResult {
118 pub fn is_success(&self) -> bool {
119 matches!(self, AuditResult::Success)
120 }
121
122 pub fn success() -> Self {
123 AuditResult::Success
124 }
125
126 pub fn failure(code: &str, message: &str) -> Self {
127 AuditResult::Failure {
128 error_code: code.to_string(),
129 error_message: message.to_string(),
130 }
131 }
132
133 pub fn denied(reason: &str) -> Self {
134 AuditResult::Denied {
135 reason: reason.to_string(),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Default, Deserialize)]
142pub struct AuditFilter {
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub user_id: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub action: Option<AuditAction>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub resource_type: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub result: Option<AuditResult>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub start_time: Option<DateTime<Utc>>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub end_time: Option<DateTime<Utc>>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub limit: Option<usize>,
164}
165
166impl AuditEntry {
167 pub fn new(user_id: &str, action: AuditAction, resource_type: &str) -> Self {
169 Self {
170 id: Uuid::new_v4(),
171 timestamp: Utc::now(),
172 user_id: user_id.to_string(),
173 session_id: None,
174 action,
175 resource_type: resource_type.to_string(),
176 resource_id: None,
177 result: AuditResult::Success,
178 details: None,
179 ip_address: None,
180 request_id: None,
181 duration_ms: None,
182 }
183 }
184
185 pub fn with_session(mut self, session_id: &str) -> Self {
187 self.session_id = Some(session_id.to_string());
188 self
189 }
190
191 pub fn with_resource_id(mut self, resource_id: &str) -> Self {
193 self.resource_id = Some(resource_id.to_string());
194 self
195 }
196
197 pub fn with_result(mut self, result: AuditResult) -> Self {
199 self.result = result;
200 self
201 }
202
203 pub fn with_details(mut self, details: serde_json::Value) -> Self {
205 self.details = Some(details);
206 self
207 }
208
209 pub fn with_ip(mut self, ip_address: &str) -> Self {
211 self.ip_address = Some(ip_address.to_string());
212 self
213 }
214
215 pub fn with_request_id(mut self, request_id: &str) -> Self {
217 self.request_id = Some(request_id.to_string());
218 self
219 }
220
221 pub fn with_duration(mut self, duration_ms: u64) -> Self {
223 self.duration_ms = Some(duration_ms);
224 self
225 }
226
227 pub fn matches_filter(&self, filter: &AuditFilter) -> bool {
229 if let Some(user_id) = &filter.user_id {
230 if self.user_id != *user_id {
231 return false;
232 }
233 }
234
235 if let Some(action) = &filter.action {
236 if self.action.as_str() != action.as_str() {
237 return false;
238 }
239 }
240
241 if let Some(resource_type) = &filter.resource_type {
242 if self.resource_type != *resource_type {
243 return false;
244 }
245 }
246
247 if let Some(start_time) = &filter.start_time {
248 if self.timestamp < *start_time {
249 return false;
250 }
251 }
252
253 if let Some(end_time) = &filter.end_time {
254 if self.timestamp > *end_time {
255 return false;
256 }
257 }
258
259 true
260 }
261}
262
263#[derive(Debug, Clone)]
265pub enum ExportFormat {
266 Json,
268 Csv,
270 Syslog,
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_audit_entry_creation() {
280 let entry = AuditEntry::new("user123", AuditAction::Read, "document");
281 assert_eq!(entry.user_id, "user123");
282 assert!(entry.result.is_success());
283 }
284
285 #[test]
286 fn test_audit_entry_with_details() {
287 let entry = AuditEntry::new("user123", AuditAction::Execute, "tool")
288 .with_details(serde_json::json!({ "tool_name": "test" }));
289
290 assert!(entry.details.is_some());
291 }
292
293 #[test]
294 fn test_audit_result() {
295 let success = AuditResult::success();
296 assert!(success.is_success());
297
298 let failure = AuditResult::failure("E001", "Test error");
299 assert!(!failure.is_success());
300 }
301
302 #[test]
303 fn test_audit_filter_matching() {
304 let entry = AuditEntry::new("user123", AuditAction::Read, "document");
305
306 let filter = AuditFilter {
307 user_id: Some("user123".to_string()),
308 ..Default::default()
309 };
310 assert!(entry.matches_filter(&filter));
311
312 let filter2 = AuditFilter {
313 user_id: Some("other_user".to_string()),
314 ..Default::default()
315 };
316 assert!(!entry.matches_filter(&filter2));
317 }
318
319 #[test]
320 fn test_serialize_audit_entry() {
321 let entry = AuditEntry::new("user123", AuditAction::Login, "session");
322 let json = serde_json::to_string(&entry).unwrap();
323 assert!(json.contains("user123"));
324 assert!(json.contains("Login")); }
326}