1use hmac::{Hmac, Mac};
25use serde::{Deserialize, Serialize};
26use sha2::Sha256;
27use std::path::PathBuf;
28use std::sync::Mutex;
29use thiserror::Error;
30use tracing::info;
31use zeroize::Zeroize;
32
33#[allow(dead_code)]
36#[derive(Error, Debug)]
37pub enum AuditError {
38 #[error("IO error: {0}")]
39 Io(#[from] std::io::Error),
40
41 #[error("Serialization error: {0}")]
42 Serialization(#[from] serde_json::Error),
43
44 #[error("HMAC verification failed at entry {0}")]
45 TamperDetected(u64),
46
47 #[error("Audit log is empty")]
48 EmptyLog,
49
50 #[error("Audit log not initialized")]
51 NotInitialized,
52}
53
54#[allow(dead_code)]
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
59#[serde(rename_all = "snake_case")]
60pub enum AuditEventType {
61 ToolCall,
63 ToolResult,
65 PolicyDecision,
67 ApprovalRequested,
69 ApprovalGranted,
71 ApprovalDenied,
73 SandboxViolation,
75 SecurityViolation,
77 AgentStart,
79 AgentFinish,
81 Error,
83 ConfigChange,
85 Custom(String),
87}
88
89#[allow(dead_code)]
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct AuditEntry {
93 pub index: u64,
95 pub timestamp: String,
97 pub event_type: AuditEventType,
99 pub component: String,
101 pub description: String,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub metadata: Option<serde_json::Value>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub session_id: Option<String>,
109 pub hmac: String,
111}
112
113#[allow(dead_code)]
115pub struct AuditLog {
116 key: Vec<u8>,
118 entries: Mutex<Vec<AuditEntry>>,
120 session_id: String,
122 trace_logging: bool,
124 file_path: Option<PathBuf>,
126}
127
128impl Drop for AuditLog {
130 fn drop(&mut self) {
131 self.key.zeroize();
132 }
133}
134
135#[allow(dead_code)]
136impl AuditLog {
137 pub fn new(session_id: String) -> Self {
139 use rand::RngCore;
140 let mut key = vec![0u8; 32];
141 rand::rngs::OsRng.fill_bytes(&mut key);
142
143 Self {
144 key,
145 entries: Mutex::new(Vec::new()),
146 session_id,
147 trace_logging: true,
148 file_path: None,
149 }
150 }
151
152 pub fn with_key(session_id: String, key: Vec<u8>) -> Self {
154 Self {
155 key,
156 entries: Mutex::new(Vec::new()),
157 session_id,
158 trace_logging: true,
159 file_path: None,
160 }
161 }
162
163 pub fn set_trace_logging(&mut self, enabled: bool) {
165 self.trace_logging = enabled;
166 }
167
168 pub fn set_file_path(&mut self, path: PathBuf) {
170 self.file_path = Some(path);
171 }
172
173 pub fn append(
175 &self,
176 event_type: AuditEventType,
177 component: &str,
178 description: &str,
179 metadata: Option<serde_json::Value>,
180 ) -> Result<AuditEntry, AuditError> {
181 let mut entries = self.lock_entries()?;
182
183 let index = entries.len() as u64;
184 let timestamp = chrono::Utc::now().to_rfc3339();
185
186 let hmac = self.compute_hmac(
188 index,
189 ×tamp,
190 &event_type,
191 component,
192 description,
193 &metadata,
194 entries.last(),
195 )?;
196
197 let entry = AuditEntry {
198 index,
199 timestamp,
200 event_type: event_type.clone(),
201 component: component.to_string(),
202 description: description.to_string(),
203 metadata,
204 session_id: Some(self.session_id.clone()),
205 hmac,
206 };
207
208 if self.trace_logging {
209 info!(
210 audit.index = entry.index,
211 audit.event = ?entry.event_type,
212 audit.component = %entry.component,
213 "Audit: {}",
214 entry.description
215 );
216 }
217
218 entries.push(entry.clone());
219
220 if let Some(path) = &self.file_path {
222 self.persist_to_file(path, &entries)?;
223 }
224
225 Ok(entry)
226 }
227
228 pub fn tool_call(
230 &self,
231 tool_name: &str,
232 args: &serde_json::Value,
233 ) -> Result<AuditEntry, AuditError> {
234 self.append(
235 AuditEventType::ToolCall,
236 tool_name,
237 &format!("Tool call: {}", tool_name),
238 Some(serde_json::json!({"arguments": args})),
239 )
240 }
241
242 pub fn tool_result(
244 &self,
245 tool_name: &str,
246 result: &crate::tools::ToolResult,
247 ) -> Result<AuditEntry, AuditError> {
248 self.append(
249 AuditEventType::ToolResult,
250 tool_name,
251 &format!("Tool result: {} (success: {})", tool_name, result.success),
252 Some(serde_json::json!({
253 "success": result.success,
254 "exit_code": result.exit_code,
255 "duration_ms": result.duration_ms,
256 "output_length": result.output.len(),
257 })),
258 )
259 }
260
261 pub fn policy_decision(
263 &self,
264 tool_name: &str,
265 allowed: bool,
266 reason: Option<&str>,
267 ) -> Result<AuditEntry, AuditError> {
268 self.append(
269 AuditEventType::PolicyDecision,
270 "policy",
271 &format!(
272 "Policy decision for '{}': {}",
273 tool_name,
274 if allowed { "allowed" } else { "denied" }
275 ),
276 Some(serde_json::json!({
277 "tool": tool_name,
278 "allowed": allowed,
279 "reason": reason,
280 })),
281 )
282 }
283
284 pub fn approval(
286 &self,
287 tool_name: &str,
288 granted: bool,
289 reason: Option<&str>,
290 ) -> Result<AuditEntry, AuditError> {
291 let event_type = if granted {
292 AuditEventType::ApprovalGranted
293 } else {
294 AuditEventType::ApprovalDenied
295 };
296
297 self.append(
298 event_type,
299 "approval",
300 &format!(
301 "Approval for '{}': {}",
302 tool_name,
303 if granted { "granted" } else { "denied" }
304 ),
305 Some(serde_json::json!({
306 "tool": tool_name,
307 "granted": granted,
308 "reason": reason,
309 })),
310 )
311 }
312
313 pub fn entries(&self) -> Result<Vec<AuditEntry>, AuditError> {
315 Ok(self.lock_entries()?.clone())
316 }
317
318 pub fn len(&self) -> Result<usize, AuditError> {
320 Ok(self.lock_entries()?.len())
321 }
322
323 pub fn is_empty(&self) -> Result<bool, AuditError> {
325 Ok(self.lock_entries()?.is_empty())
326 }
327
328 pub fn verify(&self) -> Result<(), AuditError> {
330 let entries = self.lock_entries()?;
331
332 if entries.is_empty() {
333 return Err(AuditError::EmptyLog);
334 }
335
336 let mut prev_hmac = String::new();
337
338 for entry in entries.iter() {
339 let expected_hmac = self.compute_hmac_for_verification(
340 entry.index,
341 &entry.timestamp,
342 &entry.event_type,
343 &entry.component,
344 &entry.description,
345 &entry.metadata,
346 &prev_hmac,
347 )?;
348
349 if entry.hmac != expected_hmac {
350 return Err(AuditError::TamperDetected(entry.index));
351 }
352
353 prev_hmac = entry.hmac.clone();
354 }
355
356 Ok(())
357 }
358
359 pub fn to_json(&self) -> Result<String, AuditError> {
361 let entries = self.lock_entries()?;
362 Ok(serde_json::to_string_pretty(&*entries)?)
363 }
364
365 pub fn to_json_lines(&self) -> Result<String, AuditError> {
367 let entries = self.lock_entries()?;
368 let mut lines = String::new();
369 for entry in entries.iter() {
370 lines.push_str(&serde_json::to_string(entry)?);
371 lines.push('\n');
372 }
373 Ok(lines)
374 }
375
376 fn lock_entries(&self) -> Result<std::sync::MutexGuard<'_, Vec<AuditEntry>>, AuditError> {
380 self.entries.lock().map_err(|_| AuditError::NotInitialized)
381 }
382
383 #[allow(clippy::too_many_arguments)]
384 fn compute_hmac(
385 &self,
386 index: u64,
387 timestamp: &str,
388 event_type: &AuditEventType,
389 component: &str,
390 description: &str,
391 metadata: &Option<serde_json::Value>,
392 prev_entry: Option<&AuditEntry>,
393 ) -> Result<String, AuditError> {
394 let prev_hmac = prev_entry.map(|e| e.hmac.as_str()).unwrap_or("");
395
396 self.compute_hmac_for_verification(
397 index,
398 timestamp,
399 event_type,
400 component,
401 description,
402 metadata,
403 prev_hmac,
404 )
405 }
406
407 #[allow(clippy::too_many_arguments)]
408 fn compute_hmac_for_verification(
409 &self,
410 index: u64,
411 timestamp: &str,
412 event_type: &AuditEventType,
413 component: &str,
414 description: &str,
415 metadata: &Option<serde_json::Value>,
416 prev_hmac: &str,
417 ) -> Result<String, AuditError> {
418 let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).map_err(|e| {
419 AuditError::Serialization(serde_json::Error::io(std::io::Error::new(
420 std::io::ErrorKind::InvalidInput,
421 e.to_string(),
422 )))
423 })?;
424
425 mac.update(prev_hmac.as_bytes());
426 mac.update(&index.to_le_bytes());
427 mac.update(timestamp.as_bytes());
428 mac.update(serde_json::to_string(event_type)?.as_bytes());
429 mac.update(component.as_bytes());
430 mac.update(description.as_bytes());
431
432 if let Some(m) = metadata {
433 mac.update(serde_json::to_string(m)?.as_bytes());
434 }
435
436 Ok(hex::encode(mac.finalize().into_bytes()))
437 }
438
439 fn persist_to_file(&self, path: &PathBuf, entries: &[AuditEntry]) -> Result<(), AuditError> {
440 let json = serde_json::to_string_pretty(entries)?;
441 std::fs::write(path, json)?;
442 Ok(())
443 }
444}
445
446#[cfg(test)]
449mod tests {
450 use super::*;
451
452 fn create_test_log() -> AuditLog {
453 AuditLog::with_key("test-session".to_string(), vec![0u8; 32])
454 }
455
456 #[test]
457 fn test_audit_log_empty() {
458 let log = create_test_log();
459 assert!(log.is_empty().unwrap());
460 assert_eq!(log.len().unwrap(), 0);
461 }
462
463 #[test]
464 fn test_audit_log_append() {
465 let log = create_test_log();
466 let entry = log
467 .append(AuditEventType::AgentStart, "agent", "Agent started", None)
468 .unwrap();
469
470 assert_eq!(entry.index, 0);
471 assert_eq!(entry.component, "agent");
472 assert_eq!(entry.event_type, AuditEventType::AgentStart);
473 assert!(!entry.hmac.is_empty());
474 assert_eq!(log.len().unwrap(), 1);
475 }
476
477 #[test]
478 fn test_audit_log_multiple_entries() {
479 let log = create_test_log();
480
481 log.append(AuditEventType::AgentStart, "agent", "Agent started", None)
482 .unwrap();
483
484 log.append(
485 AuditEventType::ToolCall,
486 "shell_exec",
487 "Executing command",
488 Some(serde_json::json!({"command": "echo hello"})),
489 )
490 .unwrap();
491
492 assert_eq!(log.len().unwrap(), 2);
493 }
494
495 #[test]
496 fn test_audit_log_verify_valid() {
497 let log = create_test_log();
498
499 log.append(AuditEventType::AgentStart, "agent", "Agent started", None)
500 .unwrap();
501
502 log.append(
503 AuditEventType::ToolCall,
504 "shell_exec",
505 "Executing command",
506 None,
507 )
508 .unwrap();
509
510 log.append(
511 AuditEventType::ToolResult,
512 "shell_exec",
513 "Command completed",
514 None,
515 )
516 .unwrap();
517
518 assert!(log.verify().is_ok());
519 }
520
521 #[test]
522 fn test_audit_log_verify_tampered() {
523 let log = create_test_log();
524
525 log.append(AuditEventType::AgentStart, "agent", "Agent started", None)
526 .unwrap();
527
528 log.append(
529 AuditEventType::ToolCall,
530 "shell_exec",
531 "Executing command",
532 None,
533 )
534 .unwrap();
535
536 {
538 let mut entries = log.entries.lock().unwrap();
539 entries[1].description = "Tampered!".to_string();
540 }
541
542 let result = log.verify();
543 assert!(result.is_err());
544 assert!(matches!(result.unwrap_err(), AuditError::TamperDetected(1)));
545 }
546
547 #[test]
548 fn test_audit_log_verify_empty() {
549 let log = create_test_log();
550 let result = log.verify();
551 assert!(matches!(result.unwrap_err(), AuditError::EmptyLog));
552 }
553
554 #[test]
555 fn test_audit_log_tool_call() {
556 let log = create_test_log();
557 let args = serde_json::json!({"command": "echo hello"});
558 let entry = log.tool_call("shell_exec", &args).unwrap();
559
560 assert_eq!(entry.event_type, AuditEventType::ToolCall);
561 assert_eq!(entry.component, "shell_exec");
562 }
563
564 #[test]
565 fn test_audit_log_policy_decision() {
566 let log = create_test_log();
567 let entry = log.policy_decision("shell_exec", true, None).unwrap();
568
569 assert_eq!(entry.event_type, AuditEventType::PolicyDecision);
570 assert_eq!(entry.component, "policy");
571 }
572
573 #[test]
574 fn test_audit_log_approval_granted() {
575 let log = create_test_log();
576 let entry = log
577 .approval("shell_exec", true, Some("User approved"))
578 .unwrap();
579
580 assert_eq!(entry.event_type, AuditEventType::ApprovalGranted);
581 }
582
583 #[test]
584 fn test_audit_log_approval_denied() {
585 let log = create_test_log();
586 let entry = log
587 .approval("shell_exec", false, Some("User denied"))
588 .unwrap();
589
590 assert_eq!(entry.event_type, AuditEventType::ApprovalDenied);
591 }
592
593 #[test]
594 fn test_audit_log_to_json() {
595 let log = create_test_log();
596
597 log.append(AuditEventType::AgentStart, "agent", "Agent started", None)
598 .unwrap();
599
600 let json = log.to_json().unwrap();
601 assert!(json.contains("Agent started"));
602 assert!(json.contains("agent_start"));
603 }
604
605 #[test]
606 fn test_audit_log_to_json_lines() {
607 let log = create_test_log();
608
609 log.append(AuditEventType::AgentStart, "agent", "Agent started", None)
610 .unwrap();
611
612 log.append(
613 AuditEventType::ToolCall,
614 "shell_exec",
615 "Executing command",
616 None,
617 )
618 .unwrap();
619
620 let lines = log.to_json_lines().unwrap();
621 let line_count = lines.lines().count();
622 assert_eq!(line_count, 2);
623 }
624
625 #[test]
626 fn test_audit_log_session_id() {
627 let log = AuditLog::new("my-session-123".to_string());
628 let entry = log
629 .append(AuditEventType::AgentStart, "agent", "Agent started", None)
630 .unwrap();
631
632 assert_eq!(entry.session_id.unwrap(), "my-session-123");
633 }
634
635 #[test]
636 fn test_audit_log_different_keys_produce_different_hmacs() {
637 let log1 = AuditLog::with_key("test".to_string(), vec![1u8; 32]);
638 let log2 = AuditLog::with_key("test".to_string(), vec![2u8; 32]);
639
640 let e1 = log1
641 .append(AuditEventType::AgentStart, "agent", "Agent started", None)
642 .unwrap();
643
644 let e2 = log2
645 .append(AuditEventType::AgentStart, "agent", "Agent started", None)
646 .unwrap();
647
648 assert_ne!(e1.hmac, e2.hmac);
649 }
650
651 #[test]
652 fn test_audit_entry_serialization() {
653 let entry = AuditEntry {
654 index: 0,
655 timestamp: "2026-01-01T00:00:00Z".to_string(),
656 event_type: AuditEventType::ToolCall,
657 component: "shell_exec".to_string(),
658 description: "Executed command".to_string(),
659 metadata: Some(serde_json::json!({"command": "echo hello"})),
660 session_id: Some("session-1".to_string()),
661 hmac: "abc123".to_string(),
662 };
663
664 let json = serde_json::to_string(&entry).unwrap();
665 assert!(json.contains("shell_exec"));
666 assert!(json.contains("tool_call"));
667 assert!(json.contains("echo hello"));
668 }
669
670 #[test]
671 fn test_audit_error_tamper_detected() {
672 let err = AuditError::TamperDetected(5);
673 assert_eq!(format!("{}", err), "HMAC verification failed at entry 5");
674 }
675
676 #[test]
677 fn test_audit_error_empty() {
678 let err = AuditError::EmptyLog;
679 assert_eq!(format!("{}", err), "Audit log is empty");
680 }
681
682 #[test]
683 fn test_audit_event_type_custom() {
684 let event = AuditEventType::Custom("my_event".to_string());
685 let json = serde_json::to_string(&event).unwrap();
686 assert_eq!(json, "{\"custom\":\"my_event\"}");
687 }
688
689 #[test]
690 fn test_audit_log_with_metadata() {
691 let log = create_test_log();
692 let metadata = serde_json::json!({
693 "key1": "value1",
694 "key2": 42,
695 "nested": {"a": 1}
696 });
697
698 let entry = log
699 .append(
700 AuditEventType::ConfigChange,
701 "config",
702 "Configuration changed",
703 Some(metadata),
704 )
705 .unwrap();
706
707 assert!(entry.metadata.is_some());
708 let meta = entry.metadata.unwrap();
709 assert_eq!(meta["key1"], "value1");
710 assert_eq!(meta["key2"], 42);
711 }
712
713 #[test]
714 fn test_audit_log_trace_logging_disabled() {
715 let mut log = create_test_log();
716 log.set_trace_logging(false);
717
718 let entry = log
719 .append(AuditEventType::AgentStart, "agent", "Agent started", None)
720 .unwrap();
721
722 assert_eq!(entry.index, 0);
723 }
724
725 #[test]
726 fn test_audit_log_chain_integrity() {
727 let log = create_test_log();
728
729 for i in 0..10 {
731 log.append(
732 AuditEventType::Custom(format!("event_{}", i)),
733 "test",
734 &format!("Entry {}", i),
735 None,
736 )
737 .unwrap();
738 }
739
740 assert!(log.verify().is_ok());
742
743 {
745 let mut entries = log.entries.lock().unwrap();
746 entries[3].description = "MODIFIED".to_string();
747 }
748
749 assert!(log.verify().is_err());
751 }
752}