1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuditEvent {
14 pub id: String,
16
17 pub timestamp: chrono::DateTime<chrono::Utc>,
19
20 pub action: AuditAction,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub box_id: Option<String>,
26
27 #[serde(default)]
29 pub actor: String,
30
31 pub outcome: AuditOutcome,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub message: Option<String>,
37
38 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
40 pub metadata: HashMap<String, serde_json::Value>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum AuditAction {
47 BoxCreate,
49 BoxStart,
50 BoxStop,
51 BoxDestroy,
52 BoxRestart,
53
54 ExecCommand,
56 ExecAttach,
57
58 ImagePull,
60 ImagePush,
61 ImageBuild,
62 ImageDelete,
63
64 NetworkCreate,
66 NetworkDelete,
67 NetworkConnect,
68 NetworkDisconnect,
69
70 VolumeCreate,
72 VolumeDelete,
73
74 SignatureVerify,
76 AttestationVerify,
77 SecretInject,
78 SealData,
79 UnsealData,
80
81 RegistryLogin,
83 RegistryLogout,
84
85 SystemPrune,
87 ConfigChange,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum AuditOutcome {
94 Success,
96 Failure,
98 Denied,
100}
101
102impl AuditEvent {
103 pub fn new(action: AuditAction, outcome: AuditOutcome) -> Self {
105 let timestamp = chrono::Utc::now();
106 let id = format!("audit-{}", timestamp.timestamp_nanos_opt().unwrap_or(0));
108 Self {
109 id,
110 timestamp,
111 action,
112 box_id: None,
113 actor: "cli".to_string(),
114 outcome,
115 message: None,
116 metadata: HashMap::new(),
117 }
118 }
119
120 pub fn with_box_id(mut self, box_id: impl Into<String>) -> Self {
122 self.box_id = Some(box_id.into());
123 self
124 }
125
126 pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
128 self.actor = actor.into();
129 self
130 }
131
132 pub fn with_message(mut self, message: impl Into<String>) -> Self {
134 self.message = Some(message.into());
135 self
136 }
137
138 pub fn with_metadata(
140 mut self,
141 key: impl Into<String>,
142 value: impl Into<serde_json::Value>,
143 ) -> Self {
144 self.metadata.insert(key.into(), value.into());
145 self
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct AuditConfig {
152 #[serde(default = "default_true")]
154 pub enabled: bool,
155
156 #[serde(default = "default_max_size")]
158 pub max_size: u64,
159
160 #[serde(default = "default_max_files")]
162 pub max_files: u32,
163}
164
165fn default_true() -> bool {
166 true
167}
168
169fn default_max_size() -> u64 {
170 50 * 1024 * 1024 }
172
173fn default_max_files() -> u32 {
174 10
175}
176
177impl Default for AuditConfig {
178 fn default() -> Self {
179 Self {
180 enabled: true,
181 max_size: 50 * 1024 * 1024,
182 max_files: 10,
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_audit_event_new() {
193 let event = AuditEvent::new(AuditAction::BoxCreate, AuditOutcome::Success);
194 assert_eq!(event.action, AuditAction::BoxCreate);
195 assert_eq!(event.outcome, AuditOutcome::Success);
196 assert_eq!(event.actor, "cli");
197 assert!(event.box_id.is_none());
198 assert!(!event.id.is_empty());
199 }
200
201 #[test]
202 fn test_audit_event_builder() {
203 let event = AuditEvent::new(AuditAction::ExecCommand, AuditOutcome::Success)
204 .with_box_id("box-123")
205 .with_actor("sdk")
206 .with_message("Executed /bin/ls")
207 .with_metadata("exit_code", serde_json::json!(0));
208
209 assert_eq!(event.box_id, Some("box-123".to_string()));
210 assert_eq!(event.actor, "sdk");
211 assert_eq!(event.message, Some("Executed /bin/ls".to_string()));
212 assert_eq!(event.metadata["exit_code"], serde_json::json!(0));
213 }
214
215 #[test]
216 fn test_audit_event_serde_roundtrip() {
217 let event = AuditEvent::new(AuditAction::ImagePull, AuditOutcome::Success)
218 .with_box_id("box-456")
219 .with_message("Pulled nginx:latest")
220 .with_metadata("image", serde_json::json!("nginx:latest"));
221
222 let json = serde_json::to_string(&event).unwrap();
223 let parsed: AuditEvent = serde_json::from_str(&json).unwrap();
224 assert_eq!(parsed.action, AuditAction::ImagePull);
225 assert_eq!(parsed.outcome, AuditOutcome::Success);
226 assert_eq!(parsed.box_id, Some("box-456".to_string()));
227 assert_eq!(parsed.metadata["image"], serde_json::json!("nginx:latest"));
228 }
229
230 #[test]
231 fn test_audit_action_serde() {
232 let actions = vec![
233 AuditAction::BoxCreate,
234 AuditAction::BoxStart,
235 AuditAction::BoxStop,
236 AuditAction::BoxDestroy,
237 AuditAction::ExecCommand,
238 AuditAction::ImagePull,
239 AuditAction::SignatureVerify,
240 AuditAction::SecretInject,
241 ];
242 for action in actions {
243 let json = serde_json::to_string(&action).unwrap();
244 let parsed: AuditAction = serde_json::from_str(&json).unwrap();
245 assert_eq!(parsed, action);
246 }
247 }
248
249 #[test]
250 fn test_audit_outcome_serde() {
251 let outcomes = vec![
252 AuditOutcome::Success,
253 AuditOutcome::Failure,
254 AuditOutcome::Denied,
255 ];
256 for outcome in outcomes {
257 let json = serde_json::to_string(&outcome).unwrap();
258 let parsed: AuditOutcome = serde_json::from_str(&json).unwrap();
259 assert_eq!(parsed, outcome);
260 }
261 }
262
263 #[test]
264 fn test_audit_config_default() {
265 let config = AuditConfig::default();
266 assert!(config.enabled);
267 assert_eq!(config.max_size, 50 * 1024 * 1024);
268 assert_eq!(config.max_files, 10);
269 }
270
271 #[test]
272 fn test_audit_config_serde() {
273 let config = AuditConfig {
274 enabled: false,
275 max_size: 100 * 1024 * 1024,
276 max_files: 5,
277 };
278 let json = serde_json::to_string(&config).unwrap();
279 let parsed: AuditConfig = serde_json::from_str(&json).unwrap();
280 assert!(!parsed.enabled);
281 assert_eq!(parsed.max_size, 100 * 1024 * 1024);
282 assert_eq!(parsed.max_files, 5);
283 }
284
285 #[test]
286 fn test_audit_event_empty_metadata_skipped() {
287 let event = AuditEvent::new(AuditAction::BoxStop, AuditOutcome::Success);
288 let json = serde_json::to_string(&event).unwrap();
289 assert!(!json.contains("metadata"));
291 }
292
293 #[test]
294 fn test_audit_event_none_fields_skipped() {
295 let event = AuditEvent::new(AuditAction::SystemPrune, AuditOutcome::Success);
296 let json = serde_json::to_string(&event).unwrap();
297 assert!(!json.contains("box_id"));
298 assert!(!json.contains("message"));
299 }
300
301 #[test]
302 fn test_audit_action_all_variants() {
303 let variants = vec![
305 (AuditAction::BoxCreate, "\"box_create\""),
306 (AuditAction::BoxStart, "\"box_start\""),
307 (AuditAction::BoxStop, "\"box_stop\""),
308 (AuditAction::BoxDestroy, "\"box_destroy\""),
309 (AuditAction::BoxRestart, "\"box_restart\""),
310 (AuditAction::ExecCommand, "\"exec_command\""),
311 (AuditAction::ExecAttach, "\"exec_attach\""),
312 (AuditAction::ImagePull, "\"image_pull\""),
313 (AuditAction::ImagePush, "\"image_push\""),
314 (AuditAction::ImageBuild, "\"image_build\""),
315 (AuditAction::ImageDelete, "\"image_delete\""),
316 (AuditAction::NetworkCreate, "\"network_create\""),
317 (AuditAction::NetworkDelete, "\"network_delete\""),
318 (AuditAction::NetworkConnect, "\"network_connect\""),
319 (AuditAction::NetworkDisconnect, "\"network_disconnect\""),
320 (AuditAction::VolumeCreate, "\"volume_create\""),
321 (AuditAction::VolumeDelete, "\"volume_delete\""),
322 (AuditAction::SignatureVerify, "\"signature_verify\""),
323 (AuditAction::AttestationVerify, "\"attestation_verify\""),
324 (AuditAction::SecretInject, "\"secret_inject\""),
325 (AuditAction::SealData, "\"seal_data\""),
326 (AuditAction::UnsealData, "\"unseal_data\""),
327 (AuditAction::RegistryLogin, "\"registry_login\""),
328 (AuditAction::RegistryLogout, "\"registry_logout\""),
329 (AuditAction::SystemPrune, "\"system_prune\""),
330 (AuditAction::ConfigChange, "\"config_change\""),
331 ];
332 for (action, expected) in variants {
333 let json = serde_json::to_string(&action).unwrap();
334 assert_eq!(json, expected, "Failed for {:?}", action);
335 }
336 }
337}