1use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum AuditEventType {
32 CaStart,
35 CaStop,
37 CaHealthChange,
39
40 EnrollRequest,
43 CertIssue,
45 CertReenroll,
47 EnrollReject,
49 CertRevoke,
51 CrlGenerate,
53
54 KeyGenerate,
57 KeyLoad,
59 KeyDestroy,
61
62 OtpCreate,
65 OtpUse,
67 OtpExpire,
69 OtpRevoke,
71
72 AuthSuccess,
75 AuthFailure,
77
78 AdminLogin,
81 AdminLogout,
83 AdminAction,
85
86 SecurityViolation,
89}
90
91impl AuditEventType {
92 pub fn as_str(self) -> &'static str {
94 match self {
95 AuditEventType::CaStart => "ca.start",
96 AuditEventType::CaStop => "ca.stop",
97 AuditEventType::CaHealthChange => "ca.health-change",
98 AuditEventType::EnrollRequest => "enroll.request",
99 AuditEventType::CertIssue => "cert.issue",
100 AuditEventType::CertReenroll => "cert.reenroll",
101 AuditEventType::EnrollReject => "enroll.reject",
102 AuditEventType::CertRevoke => "cert.revoke",
103 AuditEventType::CrlGenerate => "crl.generate",
104 AuditEventType::KeyGenerate => "key.generate",
105 AuditEventType::KeyLoad => "key.load",
106 AuditEventType::KeyDestroy => "key.destroy",
107 AuditEventType::OtpCreate => "otp.create",
108 AuditEventType::OtpUse => "otp.use",
109 AuditEventType::OtpExpire => "otp.expire",
110 AuditEventType::OtpRevoke => "otp.revoke",
111 AuditEventType::AuthSuccess => "auth.success",
112 AuditEventType::AuthFailure => "auth.failure",
113 AuditEventType::AdminLogin => "admin.login",
114 AuditEventType::AdminLogout => "admin.logout",
115 AuditEventType::AdminAction => "admin.action",
116 AuditEventType::SecurityViolation => "security.violation",
117 }
118 }
119}
120
121pub struct AuditEvent {
123 pub event_type: AuditEventType,
125 pub ca_id: Option<String>,
127 pub subject: Option<String>,
129 pub detail: Option<String>,
131 pub client_addr: Option<String>,
133 pub operator: Option<String>,
135}
136
137impl AuditEvent {
138 pub fn new(event_type: AuditEventType) -> Self {
140 Self {
141 event_type,
142 ca_id: None,
143 subject: None,
144 detail: None,
145 client_addr: None,
146 operator: None,
147 }
148 }
149
150 pub fn with_ca_id(mut self, ca_id: impl Into<String>) -> Self {
152 self.ca_id = Some(ca_id.into());
153 self
154 }
155
156 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
158 self.subject = Some(subject.into());
159 self
160 }
161
162 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
164 self.detail = Some(detail.into());
165 self
166 }
167
168 pub fn with_client_addr(mut self, addr: impl Into<String>) -> Self {
170 self.client_addr = Some(addr.into());
171 self
172 }
173
174 pub fn with_operator(mut self, operator: impl Into<String>) -> Self {
176 self.operator = Some(operator.into());
177 self
178 }
179}
180
181pub struct AuditState {
187 pub halted: AtomicBool,
189
190 pub violation_count: AtomicU32,
193}
194
195impl AuditState {
196 pub fn new() -> Self {
198 Self {
199 halted: AtomicBool::new(false),
200 violation_count: AtomicU32::new(0),
201 }
202 }
203
204 pub fn is_halted(&self) -> bool {
207 self.halted.load(Ordering::Relaxed)
208 }
209
210 pub fn set_halted(&self, halted: bool) {
213 self.halted.store(halted, Ordering::Relaxed);
214 }
215
216 pub fn record_violation(&self) -> u32 {
218 self.violation_count.fetch_add(1, Ordering::Relaxed) + 1
219 }
220
221 pub fn reset_violations(&self) {
223 self.violation_count.store(0, Ordering::Relaxed);
224 }
225}
226
227impl Default for AuditState {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233pub async fn record(pool: &sqlx::AnyPool, state: &AuditState, event: AuditEvent) {
239 if state.is_halted() {
241 tracing::warn!(
242 event_type = event.event_type.as_str(),
243 "audit halted — dropping event"
244 );
245 return;
246 }
247
248 let detail_json = match (&event.detail, &event.ca_id) {
252 (Some(d), Some(ca)) => Some(serde_json::json!({"detail": d, "ca_id": ca}).to_string()),
253 (Some(d), None) => Some(serde_json::json!({"detail": d}).to_string()),
254 (None, Some(ca)) => Some(serde_json::json!({"ca_id": ca}).to_string()),
255 (None, None) => None,
256 };
257
258 let sql = crate::db::pg_sql(
259 "INSERT INTO audit_events (event_type, actor, target, detail_json, source_ip, session_id) \
260 VALUES (?, ?, ?, ?, ?, ?)",
261 );
262 let result = sqlx::query(sql)
263 .bind(event.event_type.as_str())
264 .bind(&event.operator)
265 .bind(&event.subject)
266 .bind(&detail_json)
267 .bind(&event.client_addr)
268 .bind(None::<String>)
269 .execute(pool)
270 .await;
271
272 if let Err(e) = result {
273 tracing::error!(
274 event_type = event.event_type.as_str(),
275 error = %e,
276 "failed to record audit event"
277 );
278 }
279
280 if event.event_type == AuditEventType::SecurityViolation {
282 let count = state.record_violation();
283 tracing::warn!(
284 consecutive_violations = count,
285 "security violation recorded"
286 );
287 } else if event.event_type == AuditEventType::AuthSuccess {
288 state.reset_violations();
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn event_type_strings() {
298 assert_eq!(AuditEventType::CaStart.as_str(), "ca.start");
299 assert_eq!(AuditEventType::CertIssue.as_str(), "cert.issue");
300 assert_eq!(AuditEventType::OtpCreate.as_str(), "otp.create");
301 assert_eq!(AuditEventType::AuthFailure.as_str(), "auth.failure");
302 assert_eq!(AuditEventType::AdminLogin.as_str(), "admin.login");
303 assert_eq!(
304 AuditEventType::SecurityViolation.as_str(),
305 "security.violation"
306 );
307 }
308
309 #[test]
310 fn audit_state_violation_tracking() {
311 let state = AuditState::new();
312 assert_eq!(state.record_violation(), 1);
313 assert_eq!(state.record_violation(), 2);
314 state.reset_violations();
315 assert_eq!(state.record_violation(), 1);
316 }
317
318 #[test]
319 fn audit_state_halt_flag() {
320 let state = AuditState::new();
321 assert!(!state.is_halted());
322 state.set_halted(true);
323 assert!(state.is_halted());
324 state.set_halted(false);
325 assert!(!state.is_halted());
326 }
327
328 #[test]
329 fn audit_event_builder() {
330 let event = AuditEvent::new(AuditEventType::CertIssue)
331 .with_ca_id("production")
332 .with_subject("CN=device.example.com")
333 .with_detail("serial=ABC123")
334 .with_client_addr("10.0.0.1");
335
336 assert_eq!(event.event_type, AuditEventType::CertIssue);
337 assert_eq!(event.ca_id.as_deref(), Some("production"));
338 assert_eq!(event.subject.as_deref(), Some("CN=device.example.com"));
339 assert_eq!(event.detail.as_deref(), Some("serial=ABC123"));
340 assert_eq!(event.client_addr.as_deref(), Some("10.0.0.1"));
341 assert!(event.operator.is_none());
342 }
343}