1use serde::Serialize;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14#[derive(Debug, Clone, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AuditEventType {
18 ContainerStart,
19 ContainerStop,
20 ContainerExec,
21 NamespaceCreated,
22 CgroupCreated,
23 FilesystemMounted,
24 RootSwitched,
25 MountAuditPassed,
26 MountAuditFailed,
27 CapabilitiesDropped,
28 SeccompApplied,
29 SeccompProfileLoaded,
30 LandlockApplied,
31 NoNewPrivsSet,
32 NetworkBridgeSetup,
33 EgressPolicyApplied,
34 EgressDenied,
35 HealthCheckPassed,
36 HealthCheckFailed,
37 HealthCheckUnhealthy,
38 ReadinessProbeReady,
39 ReadinessProbeFailed,
40 SecretsMounted,
41 InitSupervisorStarted,
42 ZombieReaped,
43 SignalForwarded,
44 GVisorStarted,
45}
46
47#[derive(Debug, Clone, Serialize)]
49pub struct AuditEvent {
50 pub timestamp: String,
52 pub container_id: String,
54 pub container_name: String,
56 pub event_type: AuditEventType,
58 pub detail: String,
60 pub is_error: bool,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub security_posture: Option<SecurityPosture>,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct SecurityPosture {
70 pub seccomp_mode: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub landlock_abi: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub dropped_caps: Option<Vec<String>>,
78 pub gvisor: bool,
80 pub rootless: bool,
82}
83
84impl AuditEvent {
85 pub fn new(
87 container_id: &str,
88 container_name: &str,
89 event_type: AuditEventType,
90 detail: impl Into<String>,
91 ) -> Self {
92 let timestamp = SystemTime::now()
93 .duration_since(UNIX_EPOCH)
94 .map(|d| {
95 let total_secs = d.as_secs();
97 let millis = d.subsec_millis();
98
99 let days = total_secs / 86400;
101 let day_secs = total_secs % 86400;
102 let hours = day_secs / 3600;
103 let minutes = (day_secs % 3600) / 60;
104 let seconds = day_secs % 60;
105
106 let z = days as i64 + 719468;
108 let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
109 let doe = (z - era * 146097) as u64;
110 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
111 let y = yoe as i64 + era * 400;
112 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
113 let mp = (5 * doy + 2) / 153;
114 let d = doy - (153 * mp + 2) / 5 + 1;
115 let m = if mp < 10 { mp + 3 } else { mp - 9 };
116 let y = if m <= 2 { y + 1 } else { y };
117
118 format!(
119 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
120 y, m, d, hours, minutes, seconds, millis
121 )
122 })
123 .unwrap_or_else(|_| "1970-01-01T00:00:00.000Z".to_string());
124
125 Self {
126 timestamp,
127 container_id: container_id.to_string(),
128 container_name: container_name.to_string(),
129 event_type,
130 detail: detail.into(),
131 is_error: false,
132 security_posture: None,
133 }
134 }
135
136 pub fn as_error(mut self) -> Self {
138 self.is_error = true;
139 self
140 }
141
142 pub fn with_security_posture(mut self, posture: SecurityPosture) -> Self {
144 self.security_posture = Some(posture);
145 self
146 }
147
148 pub fn emit(&self) {
150 let json = serde_json::to_string(self).unwrap_or_else(|_| format!("{:?}", self));
151 if self.is_error {
152 tracing::error!(target: "nucleus::audit", "{}", json);
153 } else {
154 tracing::info!(target: "nucleus::audit", "{}", json);
155 }
156 }
157}
158
159pub fn audit(
161 container_id: &str,
162 container_name: &str,
163 event_type: AuditEventType,
164 detail: impl Into<String>,
165) {
166 AuditEvent::new(container_id, container_name, event_type, detail).emit();
167}
168
169pub fn audit_with_posture(
171 container_id: &str,
172 container_name: &str,
173 event_type: AuditEventType,
174 detail: impl Into<String>,
175 posture: SecurityPosture,
176) {
177 AuditEvent::new(container_id, container_name, event_type, detail)
178 .with_security_posture(posture)
179 .emit();
180}
181
182pub fn audit_error(
184 container_id: &str,
185 container_name: &str,
186 event_type: AuditEventType,
187 detail: impl Into<String>,
188) {
189 AuditEvent::new(container_id, container_name, event_type, detail)
190 .as_error()
191 .emit();
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_audit_event_serialization() {
200 let event = AuditEvent::new("abc123", "test", AuditEventType::ContainerStart, "started");
201 let json = serde_json::to_string(&event).unwrap();
202 assert!(json.contains("container_start"));
203 assert!(json.contains("abc123"));
204 assert!(!json.contains("security_posture"));
206 }
207
208 #[test]
209 fn test_audit_event_with_security_posture() {
210 let posture = SecurityPosture {
211 seccomp_mode: "enforce".to_string(),
212 landlock_abi: Some("V5".to_string()),
213 dropped_caps: Some(vec!["CAP_SYS_ADMIN".to_string()]),
214 gvisor: false,
215 rootless: true,
216 };
217 let event = AuditEvent::new("abc123", "test", AuditEventType::ContainerStart, "started")
218 .with_security_posture(posture);
219
220 let json = serde_json::to_string(&event).unwrap();
221 assert!(json.contains("security_posture"));
222 assert!(json.contains("enforce"));
223 assert!(json.contains("V5"));
224 assert!(json.contains("CAP_SYS_ADMIN"));
225 assert!(json.contains("\"rootless\":true"));
226 }
227
228 #[test]
229 fn test_audit_event_error_flag() {
230 let event =
231 AuditEvent::new("abc123", "test", AuditEventType::SeccompApplied, "applied").as_error();
232 assert!(event.is_error);
233 }
234}