use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
CommandExecution,
FileAccess,
ConfigChange,
AuthSuccess,
AuthFailure,
PolicyViolation,
SecurityEvent,
SigilInterception,
McpToolGated,
DelegationCrossing,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Actor {
pub channel: Option<String>,
pub user_id: Option<String>,
pub username: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Action {
pub description: String,
pub risk_level: String,
pub approved: bool,
pub allowed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExecutionResult {
pub success: bool,
pub exit_code: Option<i32>,
pub duration_ms: u64,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: String,
pub timestamp: String,
pub event_type: AuditEventType,
pub actor: Actor,
pub action: Action,
pub result: ExecutionResult,
pub signature: Option<String>,
}
impl AuditEvent {
pub fn new(event_type: AuditEventType) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
event_type,
actor: Actor::default(),
action: Action::default(),
result: ExecutionResult::default(),
signature: None,
}
}
pub fn with_actor(
mut self,
channel: String,
user_id: Option<String>,
username: Option<String>,
) -> Self {
self.actor = Actor {
channel: Some(channel),
user_id,
username,
};
self
}
pub fn with_action(
mut self,
description: String,
risk_level: String,
approved: bool,
allowed: bool,
) -> Self {
self.action = Action {
description,
risk_level,
approved,
allowed,
};
self
}
pub fn with_result(
mut self,
success: bool,
exit_code: Option<i32>,
duration_ms: u64,
error: Option<String>,
) -> Self {
self.result = ExecutionResult {
success,
exit_code,
duration_ms,
error,
};
self
}
}
pub trait AuditLogger: Send + Sync {
fn log(&self, event: &AuditEvent) -> anyhow::Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audit_event_creates_unique_ids() {
let e1 = AuditEvent::new(AuditEventType::CommandExecution);
let e2 = AuditEvent::new(AuditEventType::CommandExecution);
assert_ne!(e1.id, e2.id);
}
#[test]
fn audit_event_serializes_to_json() {
let event = AuditEvent::new(AuditEventType::SigilInterception)
.with_actor("cli".into(), Some("u1".into()), Some("alice".into()))
.with_action("Redacted IBAN".into(), "high".into(), true, true);
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("sigil_interception"));
assert!(json.contains("alice"));
}
#[test]
fn audit_event_type_variants_exhaustive() {
let types = vec![
AuditEventType::CommandExecution,
AuditEventType::FileAccess,
AuditEventType::ConfigChange,
AuditEventType::AuthSuccess,
AuditEventType::AuthFailure,
AuditEventType::PolicyViolation,
AuditEventType::SecurityEvent,
AuditEventType::SigilInterception,
AuditEventType::McpToolGated,
AuditEventType::DelegationCrossing,
];
assert_eq!(types.len(), 10);
}
}