1use serde::{Deserialize, Serialize};
4use sha2::Sha256;
5use std::collections::HashMap;
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AuditDecision {
12 Allow,
14 Deny,
16}
17
18impl fmt::Display for AuditDecision {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Allow => write!(f, "allow"),
22 Self::Deny => write!(f, "deny"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum AuditAction {
31 Write,
34 Read,
36 Delete,
38 Admin,
40 CreateRollup,
42 Forecast,
44 DetectAnomalies,
46 Subscribe,
48
49 LoginSuccess,
52 LoginFailure,
54 KeyRotation,
56 TokenRefresh,
58
59 TriggerCreate,
62 TriggerDrop,
64 SignalFired,
66
67 SchemaChange,
70 DropRollup,
72 DataExport,
74 PermissionChange,
76 PolicyLoad,
78 ApiKeyCreate,
80 ApiKeyRevoke,
82 NamespaceCreate,
84 NamespaceDelete,
86 QuotaChange,
88
89 Custom(String),
91}
92
93impl fmt::Display for AuditAction {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::Write => write!(f, "write"),
97 Self::Read => write!(f, "read"),
98 Self::Delete => write!(f, "delete"),
99 Self::Admin => write!(f, "admin"),
100 Self::CreateRollup => write!(f, "create_rollup"),
101 Self::Forecast => write!(f, "forecast"),
102 Self::DetectAnomalies => write!(f, "detect_anomalies"),
103 Self::Subscribe => write!(f, "subscribe"),
104 Self::LoginSuccess => write!(f, "login_success"),
105 Self::LoginFailure => write!(f, "login_failure"),
106 Self::KeyRotation => write!(f, "key_rotation"),
107 Self::TokenRefresh => write!(f, "token_refresh"),
108 Self::TriggerCreate => write!(f, "trigger_create"),
109 Self::TriggerDrop => write!(f, "trigger_drop"),
110 Self::SignalFired => write!(f, "signal_fired"),
111 Self::SchemaChange => write!(f, "schema_change"),
112 Self::DropRollup => write!(f, "drop_rollup"),
113 Self::DataExport => write!(f, "data_export"),
114 Self::PermissionChange => write!(f, "permission_change"),
115 Self::PolicyLoad => write!(f, "policy_load"),
116 Self::ApiKeyCreate => write!(f, "api_key_create"),
117 Self::ApiKeyRevoke => write!(f, "api_key_revoke"),
118 Self::NamespaceCreate => write!(f, "namespace_create"),
119 Self::NamespaceDelete => write!(f, "namespace_delete"),
120 Self::QuotaChange => write!(f, "quota_change"),
121 Self::Custom(s) => write!(f, "{s}"),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AuditEvent {
133 pub id: u64,
135 pub timestamp: i64,
137 pub principal: String,
139 pub action: AuditAction,
141 pub resource: String,
143 pub decision: AuditDecision,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub source_ip: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub request_id: Option<String>,
151 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
153 pub metadata: HashMap<String, String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub prev_hash: Option<String>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub event_hash: Option<String>,
161}
162
163impl AuditEvent {
164 #[must_use]
166 pub fn new(
167 principal: impl Into<String>,
168 action: AuditAction,
169 resource: impl Into<String>,
170 decision: AuditDecision,
171 ) -> Self {
172 Self {
173 id: 0, timestamp: now_nanos(),
175 principal: principal.into(),
176 action,
177 resource: resource.into(),
178 decision,
179 source_ip: None,
180 request_id: None,
181 metadata: HashMap::new(),
182 prev_hash: None,
183 event_hash: None,
184 }
185 }
186
187 #[must_use]
189 pub fn with_source_ip(mut self, ip: impl Into<String>) -> Self {
190 self.source_ip = Some(ip.into());
191 self
192 }
193
194 #[must_use]
196 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
197 self.request_id = Some(id.into());
198 self
199 }
200
201 #[must_use]
203 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
204 self.metadata.insert(key.into(), value.into());
205 self
206 }
207
208 #[must_use]
210 pub fn with_timestamp(mut self, ts: i64) -> Self {
211 self.timestamp = ts;
212 self
213 }
214}
215
216impl fmt::Display for AuditEvent {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(
219 f,
220 "[{}] principal={} action={} resource={} decision={}",
221 self.timestamp, self.principal, self.action, self.resource, self.decision,
222 )
223 }
224}
225
226fn now_nanos() -> i64 {
238 use std::sync::atomic::{AtomicI64, Ordering};
239 static LAST: AtomicI64 = AtomicI64::new(0);
240
241 let wall = i64::try_from(
242 std::time::SystemTime::now()
243 .duration_since(std::time::UNIX_EPOCH)
244 .unwrap_or_default()
245 .as_nanos(),
246 )
247 .unwrap_or(i64::MAX);
248
249 loop {
251 let prev = LAST.load(Ordering::SeqCst);
252 let next = wall.max(prev + 1);
253 if LAST
254 .compare_exchange_weak(prev, next, Ordering::SeqCst, Ordering::Relaxed)
255 .is_ok()
256 {
257 return next;
258 }
259 }
260}
261
262impl AuditEvent {
265 fn canonical_payload(&self) -> Vec<u8> {
271 let mut buf = format!(
272 "{}|{}|{}|{}|{}|{}",
273 self.id, self.timestamp, self.principal, self.action, self.resource, self.decision,
274 )
275 .into_bytes();
276
277 let mut keys: Vec<&String> = self.metadata.keys().collect();
279 keys.sort();
280 for key in keys {
281 buf.push(b'|');
282 buf.extend_from_slice(key.as_bytes());
283 buf.push(b'=');
284 buf.extend_from_slice(self.metadata[key].as_bytes());
285 }
286
287 buf
288 }
289
290 pub fn seal(&mut self, prev_hash: Option<&str>) {
297 self.seal_with_key(prev_hash, None);
298 }
299
300 pub fn seal_with_key(&mut self, prev_hash: Option<&str>, hmac_key: Option<&[u8]>) {
305 self.prev_hash = prev_hash.map(String::from);
306
307 let hash = if let Some(key) = hmac_key {
308 use hmac::{Hmac, KeyInit, Mac};
309 type HmacSha256 = Hmac<Sha256>;
310 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
311 if let Some(ph) = &self.prev_hash {
312 mac.update(ph.as_bytes());
313 }
314 mac.update(&self.canonical_payload());
315 hex_encode(&mac.finalize().into_bytes())
316 } else {
317 use sha2::Digest;
318 let mut hasher = Sha256::new();
319 if let Some(ph) = &self.prev_hash {
320 hasher.update(ph.as_bytes());
321 }
322 hasher.update(self.canonical_payload());
323 hex_encode(&hasher.finalize())
324 };
325
326 self.event_hash = Some(hash);
327 }
328}
329
330pub fn verify_hash_chain(events: &[AuditEvent]) -> std::result::Result<(), String> {
338 verify_hash_chain_with_key(events, None)
339}
340
341pub fn verify_hash_chain_with_key(
343 events: &[AuditEvent],
344 hmac_key: Option<&[u8]>,
345) -> std::result::Result<(), String> {
346 verify_chain_from(events, None, hmac_key)
347}
348
349pub fn verify_chain_from(
357 events: &[AuditEvent],
358 expected_first_prev: Option<&str>,
359 hmac_key: Option<&[u8]>,
360) -> std::result::Result<(), String> {
361 let mut expected_prev: Option<&str> = expected_first_prev;
362
363 for (idx, event) in events.iter().enumerate() {
364 if event.prev_hash.as_deref() != expected_prev {
366 return Err(format!(
367 "event id={} (index {idx}): prev_hash mismatch — expected {:?}, got {:?}",
368 event.id, expected_prev, event.prev_hash,
369 ));
370 }
371
372 let expected_hash = if let Some(key) = hmac_key {
374 use hmac::{Hmac, KeyInit, Mac};
375 type HmacSha256 = Hmac<Sha256>;
376 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
377 if let Some(ph) = &event.prev_hash {
378 mac.update(ph.as_bytes());
379 }
380 mac.update(&event.canonical_payload());
381 hex_encode(&mac.finalize().into_bytes())
382 } else {
383 use sha2::Digest;
384 let mut hasher = Sha256::new();
385 if let Some(ph) = &event.prev_hash {
386 hasher.update(ph.as_bytes());
387 }
388 hasher.update(event.canonical_payload());
389 hex_encode(&hasher.finalize())
390 };
391
392 let actual = event.event_hash.as_deref().unwrap_or("");
393 if actual != expected_hash {
394 return Err(format!(
395 "event id={} (index {idx}): event_hash mismatch — expected {expected_hash}, got {actual}",
396 event.id,
397 ));
398 }
399
400 expected_prev = event.event_hash.as_deref();
401 }
402
403 Ok(())
404}
405
406fn hex_encode(bytes: &[u8]) -> String {
408 let mut s = String::with_capacity(bytes.len() * 2);
409 for &b in bytes {
410 use std::fmt::Write;
411 let _ = write!(s, "{b:02x}");
412 }
413 s
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn audit_event_creation() {
422 let event = AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
423 .with_source_ip("10.0.0.1")
424 .with_request_id("req-123");
425 assert_eq!(event.principal, "alice");
426 assert_eq!(event.resource, "cpu");
427 assert_eq!(event.decision, AuditDecision::Allow);
428 assert_eq!(event.source_ip.as_deref(), Some("10.0.0.1"));
429 assert_eq!(event.request_id.as_deref(), Some("req-123"));
430 }
431
432 #[test]
433 fn audit_event_with_metadata() {
434 let event = AuditEvent::new("bob", AuditAction::Delete, "logs", AuditDecision::Deny)
435 .with_metadata("reason", "insufficient privileges");
436 assert_eq!(
437 event.metadata.get("reason").unwrap(),
438 "insufficient privileges"
439 );
440 }
441
442 #[test]
443 fn audit_event_serialization() {
444 let event = AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
445 .with_timestamp(1_700_000_000_000);
446 let json = serde_json::to_string(&event).unwrap();
447 assert!(json.contains("\"principal\":\"alice\""));
448 assert!(json.contains("\"action\":\"read\""));
449 assert!(json.contains("\"decision\":\"allow\""));
450
451 let deserialized: AuditEvent = serde_json::from_str(&json).unwrap();
452 assert_eq!(deserialized.principal, "alice");
453 assert_eq!(deserialized.timestamp, 1_700_000_000_000);
454 }
455
456 #[test]
457 fn audit_event_display() {
458 let event = AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
459 .with_timestamp(100);
460 let s = event.to_string();
461 assert!(s.contains("principal=alice"));
462 assert!(s.contains("action=write"));
463 assert!(s.contains("decision=allow"));
464 }
465
466 #[test]
467 fn audit_action_display() {
468 assert_eq!(AuditAction::LoginSuccess.to_string(), "login_success");
469 assert_eq!(AuditAction::KeyRotation.to_string(), "key_rotation");
470 assert_eq!(AuditAction::Custom("foo".into()).to_string(), "foo");
471 }
472
473 #[test]
474 fn decision_display() {
475 assert_eq!(AuditDecision::Allow.to_string(), "allow");
476 assert_eq!(AuditDecision::Deny.to_string(), "deny");
477 }
478
479 #[test]
480 fn auth_actions_roundtrip() {
481 let actions = vec![
482 AuditAction::Write,
483 AuditAction::LoginFailure,
484 AuditAction::TriggerCreate,
485 AuditAction::SignalFired,
486 AuditAction::Custom("custom_op".into()),
487 ];
488 for action in actions {
489 let event = AuditEvent::new("user", action, "res", AuditDecision::Allow);
490 let json = serde_json::to_string(&event).unwrap();
491 let de: AuditEvent = serde_json::from_str(&json).unwrap();
492 assert_eq!(de.action, event.action);
493 }
494 }
495
496 #[test]
497 fn hash_chain_seal_and_verify() {
498 let mut events: Vec<AuditEvent> = Vec::new();
499 let mut prev_hash: Option<String> = None;
500
501 for i in 0..5 {
502 let mut event =
503 AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
504 .with_timestamp(1000 + i);
505 event.id = i as u64;
506 event.seal(prev_hash.as_deref());
507 prev_hash = event.event_hash.clone();
508 events.push(event);
509 }
510
511 for event in &events {
513 assert!(event.event_hash.is_some());
514 }
515 assert!(events[0].prev_hash.is_none());
517 for i in 1..events.len() {
519 assert_eq!(events[i].prev_hash, events[i - 1].event_hash);
520 }
521
522 assert!(verify_hash_chain(&events).is_ok());
524 }
525
526 #[test]
527 fn hash_chain_detects_tampering() {
528 let mut events: Vec<AuditEvent> = Vec::new();
529 let mut prev_hash: Option<String> = None;
530
531 for i in 0..3 {
532 let mut event =
533 AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
534 .with_timestamp(1000 + i);
535 event.id = i as u64;
536 event.seal(prev_hash.as_deref());
537 prev_hash = event.event_hash.clone();
538 events.push(event);
539 }
540
541 events[1].principal = "mallory".to_string();
543 assert!(verify_hash_chain(&events).is_err());
544 }
545
546 #[test]
547 fn hash_chain_empty_is_valid() {
548 assert!(verify_hash_chain(&[]).is_ok());
549 }
550
551 #[test]
552 fn sealed_event_roundtrips_json() {
553 let mut event = AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
554 .with_timestamp(42);
555 event.id = 1;
556 event.seal(None);
557
558 let json = serde_json::to_string(&event).unwrap();
559 let de: AuditEvent = serde_json::from_str(&json).unwrap();
560 assert_eq!(de.event_hash, event.event_hash);
561 assert_eq!(de.prev_hash, event.prev_hash);
562 }
563
564 #[test]
565 fn now_nanos_produces_strictly_increasing_timestamps() {
566 let mut prev = 0_i64;
569 for _ in 0..1000 {
570 let ts = now_nanos();
571 assert!(ts > prev, "timestamp {ts} must be > previous {prev}");
572 prev = ts;
573 }
574 }
575
576 #[test]
577 fn now_nanos_concurrent_uniqueness() {
578 use std::collections::HashSet;
579 use std::sync::Arc;
580
581 let timestamps = Arc::new(parking_lot::Mutex::new(Vec::new()));
582 let handles: Vec<_> = (0..4)
583 .map(|_| {
584 let ts = Arc::clone(×tamps);
585 std::thread::spawn(move || {
586 let mut local = Vec::with_capacity(250);
587 for _ in 0..250 {
588 local.push(now_nanos());
589 }
590 ts.lock().extend(local);
591 })
592 })
593 .collect();
594 for h in handles {
595 h.join().unwrap();
596 }
597 let all = timestamps.lock();
598 let unique: HashSet<_> = all.iter().copied().collect();
599 assert_eq!(
600 unique.len(),
601 all.len(),
602 "all {} timestamps must be unique, got {} unique",
603 all.len(),
604 unique.len()
605 );
606 }
607}