1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum AuditEventType {
12 CommandExecution,
14 FileAccess,
16 ConfigChange,
18 AuthSuccess,
20 AuthFailure,
22 PolicyViolation,
24 SecurityEvent,
26 SigilInterception,
28 McpToolGated,
30 DelegationCrossing,
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct Actor {
37 pub channel: Option<String>,
39 pub user_id: Option<String>,
41 pub username: Option<String>,
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct Action {
48 pub description: String,
50 pub risk_level: String,
52 pub approved: bool,
54 pub allowed: bool,
56}
57
58#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct ExecutionResult {
61 pub success: bool,
63 pub exit_code: Option<i32>,
65 pub duration_ms: u64,
67 pub error: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AuditEvent {
77 pub id: String,
79 pub timestamp: String,
81 pub event_type: AuditEventType,
83 pub actor: Actor,
85 pub action: Action,
87 pub result: ExecutionResult,
89 pub signature: Option<String>,
91}
92
93impl AuditEvent {
94 pub fn new(event_type: AuditEventType) -> Self {
96 Self {
97 id: uuid::Uuid::new_v4().to_string(),
98 timestamp: chrono::Utc::now().to_rfc3339(),
99 event_type,
100 actor: Actor::default(),
101 action: Action::default(),
102 result: ExecutionResult::default(),
103 signature: None,
104 }
105 }
106
107 pub fn with_actor(
109 mut self,
110 channel: String,
111 user_id: Option<String>,
112 username: Option<String>,
113 ) -> Self {
114 self.actor = Actor {
115 channel: Some(channel),
116 user_id,
117 username,
118 };
119 self
120 }
121
122 pub fn with_action(
124 mut self,
125 description: String,
126 risk_level: String,
127 approved: bool,
128 allowed: bool,
129 ) -> Self {
130 self.action = Action {
131 description,
132 risk_level,
133 approved,
134 allowed,
135 };
136 self
137 }
138
139 pub fn with_result(
141 mut self,
142 success: bool,
143 exit_code: Option<i32>,
144 duration_ms: u64,
145 error: Option<String>,
146 ) -> Self {
147 self.result = ExecutionResult {
148 success,
149 exit_code,
150 duration_ms,
151 error,
152 };
153 self
154 }
155}
156
157pub trait AuditLogger: Send + Sync {
162 fn log(&self, event: &AuditEvent) -> anyhow::Result<()>;
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn audit_event_creates_unique_ids() {
172 let e1 = AuditEvent::new(AuditEventType::CommandExecution);
173 let e2 = AuditEvent::new(AuditEventType::CommandExecution);
174 assert_ne!(e1.id, e2.id);
175 }
176
177 #[test]
178 fn audit_event_serializes_to_json() {
179 let event = AuditEvent::new(AuditEventType::SigilInterception)
180 .with_actor("cli".into(), Some("u1".into()), Some("alice".into()))
181 .with_action("Redacted IBAN".into(), "high".into(), true, true);
182
183 let json = serde_json::to_string(&event).unwrap();
184 assert!(json.contains("sigil_interception"));
185 assert!(json.contains("alice"));
186 }
187
188 #[test]
189 fn audit_event_type_variants_exhaustive() {
190 let types = vec![
192 AuditEventType::CommandExecution,
193 AuditEventType::FileAccess,
194 AuditEventType::ConfigChange,
195 AuditEventType::AuthSuccess,
196 AuditEventType::AuthFailure,
197 AuditEventType::PolicyViolation,
198 AuditEventType::SecurityEvent,
199 AuditEventType::SigilInterception,
200 AuditEventType::McpToolGated,
201 AuditEventType::DelegationCrossing,
202 ];
203 assert_eq!(types.len(), 10);
204 }
205}