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)]
37#[serde(rename_all = "snake_case")]
38pub enum AuditAction {
39 Write,
42 Read,
44 Delete,
46 SchemaChange,
48 DataExport,
50
51 LoginFailure,
58
59 Admin,
62 ApiKeyCreate,
64 ApiKeyRevoke,
66 PolicyLoad,
68 NamespaceCreate,
70 NamespaceDelete,
72 QuotaChange,
74
75 CreateRollup,
78 DropRollup,
80 TriggerCreate,
82 TriggerDrop,
84 SignalFired,
86
87 Custom(String),
89}
90
91impl AuditAction {
92 pub const ALL: &'static [Self] = &[
94 Self::Write,
95 Self::Read,
96 Self::Delete,
97 Self::SchemaChange,
98 Self::DataExport,
99 Self::LoginFailure,
100 Self::Admin,
101 Self::ApiKeyCreate,
102 Self::ApiKeyRevoke,
103 Self::PolicyLoad,
104 Self::NamespaceCreate,
105 Self::NamespaceDelete,
106 Self::QuotaChange,
107 Self::CreateRollup,
108 Self::DropRollup,
109 Self::TriggerCreate,
110 Self::TriggerDrop,
111 Self::SignalFired,
112 ];
113}
114
115impl fmt::Display for AuditAction {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 Self::Write => write!(f, "write"),
119 Self::Read => write!(f, "read"),
120 Self::Delete => write!(f, "delete"),
121 Self::SchemaChange => write!(f, "schema_change"),
122 Self::DataExport => write!(f, "data_export"),
123 Self::LoginFailure => write!(f, "login_failure"),
124 Self::Admin => write!(f, "admin"),
125 Self::ApiKeyCreate => write!(f, "api_key_create"),
126 Self::ApiKeyRevoke => write!(f, "api_key_revoke"),
127 Self::PolicyLoad => write!(f, "policy_load"),
128 Self::NamespaceCreate => write!(f, "namespace_create"),
129 Self::NamespaceDelete => write!(f, "namespace_delete"),
130 Self::QuotaChange => write!(f, "quota_change"),
131 Self::CreateRollup => write!(f, "create_rollup"),
132 Self::DropRollup => write!(f, "drop_rollup"),
133 Self::TriggerCreate => write!(f, "trigger_create"),
134 Self::TriggerDrop => write!(f, "trigger_drop"),
135 Self::SignalFired => write!(f, "signal_fired"),
136 Self::Custom(s) => write!(f, "{s}"),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct AuditEvent {
148 pub id: u64,
150 pub timestamp: i64,
152 pub principal: String,
154 pub action: AuditAction,
156 pub resource: String,
158 pub decision: AuditDecision,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub source_ip: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub request_id: Option<String>,
166 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
168 pub metadata: HashMap<String, String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub prev_hash: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub event_hash: Option<String>,
176}
177
178impl AuditEvent {
179 #[must_use]
181 pub fn new(
182 principal: impl Into<String>,
183 action: AuditAction,
184 resource: impl Into<String>,
185 decision: AuditDecision,
186 ) -> Self {
187 Self {
188 id: 0, timestamp: now_nanos(),
190 principal: principal.into(),
191 action,
192 resource: resource.into(),
193 decision,
194 source_ip: None,
195 request_id: None,
196 metadata: HashMap::new(),
197 prev_hash: None,
198 event_hash: None,
199 }
200 }
201
202 #[must_use]
204 pub fn with_source_ip(mut self, ip: impl Into<String>) -> Self {
205 self.source_ip = Some(ip.into());
206 self
207 }
208
209 #[must_use]
211 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
212 self.request_id = Some(id.into());
213 self
214 }
215
216 #[must_use]
218 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
219 self.metadata.insert(key.into(), value.into());
220 self
221 }
222
223 #[must_use]
225 pub fn with_timestamp(mut self, ts: i64) -> Self {
226 self.timestamp = ts;
227 self
228 }
229}
230
231impl fmt::Display for AuditEvent {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 write!(
234 f,
235 "[{}] principal={} action={} resource={} decision={}",
236 self.timestamp, self.principal, self.action, self.resource, self.decision,
237 )
238 }
239}
240
241fn now_nanos() -> i64 {
253 use std::sync::atomic::{AtomicI64, Ordering};
254 static LAST: AtomicI64 = AtomicI64::new(0);
255
256 let wall = i64::try_from(
257 std::time::SystemTime::now()
258 .duration_since(std::time::UNIX_EPOCH)
259 .unwrap_or_default()
260 .as_nanos(),
261 )
262 .unwrap_or(i64::MAX);
263
264 loop {
266 let prev = LAST.load(Ordering::SeqCst);
267 let next = wall.max(prev + 1);
268 if LAST
269 .compare_exchange_weak(prev, next, Ordering::SeqCst, Ordering::Relaxed)
270 .is_ok()
271 {
272 return next;
273 }
274 }
275}
276
277impl AuditEvent {
280 fn canonical_payload(&self) -> Vec<u8> {
286 let mut buf = format!(
287 "{}|{}|{}|{}|{}|{}",
288 self.id, self.timestamp, self.principal, self.action, self.resource, self.decision,
289 )
290 .into_bytes();
291
292 let mut keys: Vec<&String> = self.metadata.keys().collect();
294 keys.sort();
295 for key in keys {
296 buf.push(b'|');
297 buf.extend_from_slice(key.as_bytes());
298 buf.push(b'=');
299 buf.extend_from_slice(self.metadata[key].as_bytes());
300 }
301
302 buf
303 }
304
305 pub fn seal(&mut self, prev_hash: Option<&str>) {
312 self.seal_with_key(prev_hash, None);
313 }
314
315 pub fn seal_with_key(&mut self, prev_hash: Option<&str>, hmac_key: Option<&[u8]>) {
320 self.prev_hash = prev_hash.map(String::from);
321
322 let hash = if let Some(key) = hmac_key {
323 use hmac::{Hmac, KeyInit, Mac};
324 type HmacSha256 = Hmac<Sha256>;
325 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
326 if let Some(ph) = &self.prev_hash {
327 mac.update(ph.as_bytes());
328 }
329 mac.update(&self.canonical_payload());
330 hex_encode(&mac.finalize().into_bytes())
331 } else {
332 use sha2::Digest;
333 let mut hasher = Sha256::new();
334 if let Some(ph) = &self.prev_hash {
335 hasher.update(ph.as_bytes());
336 }
337 hasher.update(self.canonical_payload());
338 hex_encode(&hasher.finalize())
339 };
340
341 self.event_hash = Some(hash);
342 }
343}
344
345pub fn verify_hash_chain(events: &[AuditEvent]) -> std::result::Result<(), String> {
353 verify_hash_chain_with_key(events, None)
354}
355
356pub fn verify_hash_chain_with_key(
358 events: &[AuditEvent],
359 hmac_key: Option<&[u8]>,
360) -> std::result::Result<(), String> {
361 verify_chain_from(events, None, hmac_key)
362}
363
364pub fn verify_chain_from(
372 events: &[AuditEvent],
373 expected_first_prev: Option<&str>,
374 hmac_key: Option<&[u8]>,
375) -> std::result::Result<(), String> {
376 let mut expected_prev: Option<&str> = expected_first_prev;
377
378 for (idx, event) in events.iter().enumerate() {
379 if event.prev_hash.as_deref() != expected_prev {
381 return Err(format!(
382 "event id={} (index {idx}): prev_hash mismatch — expected {:?}, got {:?}",
383 event.id, expected_prev, event.prev_hash,
384 ));
385 }
386
387 let expected_hash = if let Some(key) = hmac_key {
389 use hmac::{Hmac, KeyInit, Mac};
390 type HmacSha256 = Hmac<Sha256>;
391 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
392 if let Some(ph) = &event.prev_hash {
393 mac.update(ph.as_bytes());
394 }
395 mac.update(&event.canonical_payload());
396 hex_encode(&mac.finalize().into_bytes())
397 } else {
398 use sha2::Digest;
399 let mut hasher = Sha256::new();
400 if let Some(ph) = &event.prev_hash {
401 hasher.update(ph.as_bytes());
402 }
403 hasher.update(event.canonical_payload());
404 hex_encode(&hasher.finalize())
405 };
406
407 let actual = event.event_hash.as_deref().unwrap_or("");
408 if actual != expected_hash {
409 return Err(format!(
410 "event id={} (index {idx}): event_hash mismatch — expected {expected_hash}, got {actual}",
411 event.id,
412 ));
413 }
414
415 expected_prev = event.event_hash.as_deref();
416 }
417
418 Ok(())
419}
420
421fn hex_encode(bytes: &[u8]) -> String {
423 let mut s = String::with_capacity(bytes.len() * 2);
424 for &b in bytes {
425 use std::fmt::Write;
426 let _ = write!(s, "{b:02x}");
427 }
428 s
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn audit_event_creation() {
437 let event = AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
438 .with_source_ip("10.0.0.1")
439 .with_request_id("req-123");
440 assert_eq!(event.principal, "alice");
441 assert_eq!(event.resource, "cpu");
442 assert_eq!(event.decision, AuditDecision::Allow);
443 assert_eq!(event.source_ip.as_deref(), Some("10.0.0.1"));
444 assert_eq!(event.request_id.as_deref(), Some("req-123"));
445 }
446
447 #[test]
448 fn audit_event_with_metadata() {
449 let event = AuditEvent::new("bob", AuditAction::Delete, "logs", AuditDecision::Deny)
450 .with_metadata("reason", "insufficient privileges");
451 assert_eq!(
452 event.metadata.get("reason").unwrap(),
453 "insufficient privileges"
454 );
455 }
456
457 #[test]
458 fn audit_event_serialization() {
459 let event = AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
460 .with_timestamp(1_700_000_000_000);
461 let json = serde_json::to_string(&event).unwrap();
462 assert!(json.contains("\"principal\":\"alice\""));
463 assert!(json.contains("\"action\":\"read\""));
464 assert!(json.contains("\"decision\":\"allow\""));
465
466 let deserialized: AuditEvent = serde_json::from_str(&json).unwrap();
467 assert_eq!(deserialized.principal, "alice");
468 assert_eq!(deserialized.timestamp, 1_700_000_000_000);
469 }
470
471 #[test]
472 fn audit_event_display() {
473 let event = AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
474 .with_timestamp(100);
475 let s = event.to_string();
476 assert!(s.contains("principal=alice"));
477 assert!(s.contains("action=write"));
478 assert!(s.contains("decision=allow"));
479 }
480
481 #[test]
482 fn audit_action_display() {
483 assert_eq!(AuditAction::LoginFailure.to_string(), "login_failure");
484 assert_eq!(AuditAction::ApiKeyCreate.to_string(), "api_key_create");
485 assert_eq!(AuditAction::Custom("foo".into()).to_string(), "foo");
486 }
487
488 #[test]
489 fn decision_display() {
490 assert_eq!(AuditDecision::Allow.to_string(), "allow");
491 assert_eq!(AuditDecision::Deny.to_string(), "deny");
492 }
493
494 #[test]
495 fn auth_actions_roundtrip() {
496 let actions = vec![
497 AuditAction::Write,
498 AuditAction::LoginFailure,
499 AuditAction::TriggerCreate,
500 AuditAction::SignalFired,
501 AuditAction::Custom("custom_op".into()),
502 ];
503 for action in actions {
504 let event = AuditEvent::new("user", action, "res", AuditDecision::Allow);
505 let json = serde_json::to_string(&event).unwrap();
506 let de: AuditEvent = serde_json::from_str(&json).unwrap();
507 assert_eq!(de.action, event.action);
508 }
509 }
510
511 #[test]
512 fn hash_chain_seal_and_verify() {
513 let mut events: Vec<AuditEvent> = Vec::new();
514 let mut prev_hash: Option<String> = None;
515
516 for i in 0..5 {
517 let mut event =
518 AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
519 .with_timestamp(1000 + i);
520 event.id = i as u64;
521 event.seal(prev_hash.as_deref());
522 prev_hash = event.event_hash.clone();
523 events.push(event);
524 }
525
526 for event in &events {
528 assert!(event.event_hash.is_some());
529 }
530 assert!(events[0].prev_hash.is_none());
532 for i in 1..events.len() {
534 assert_eq!(events[i].prev_hash, events[i - 1].event_hash);
535 }
536
537 assert!(verify_hash_chain(&events).is_ok());
539 }
540
541 #[test]
542 fn hash_chain_detects_tampering() {
543 let mut events: Vec<AuditEvent> = Vec::new();
544 let mut prev_hash: Option<String> = None;
545
546 for i in 0..3 {
547 let mut event =
548 AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
549 .with_timestamp(1000 + i);
550 event.id = i as u64;
551 event.seal(prev_hash.as_deref());
552 prev_hash = event.event_hash.clone();
553 events.push(event);
554 }
555
556 events[1].principal = "mallory".to_string();
558 assert!(verify_hash_chain(&events).is_err());
559 }
560
561 #[test]
562 fn hash_chain_empty_is_valid() {
563 assert!(verify_hash_chain(&[]).is_ok());
564 }
565
566 #[test]
567 fn sealed_event_roundtrips_json() {
568 let mut event = AuditEvent::new("alice", AuditAction::Read, "cpu", AuditDecision::Allow)
569 .with_timestamp(42);
570 event.id = 1;
571 event.seal(None);
572
573 let json = serde_json::to_string(&event).unwrap();
574 let de: AuditEvent = serde_json::from_str(&json).unwrap();
575 assert_eq!(de.event_hash, event.event_hash);
576 assert_eq!(de.prev_hash, event.prev_hash);
577 }
578
579 #[test]
580 fn now_nanos_produces_strictly_increasing_timestamps() {
581 let mut prev = 0_i64;
584 for _ in 0..1000 {
585 let ts = now_nanos();
586 assert!(ts > prev, "timestamp {ts} must be > previous {prev}");
587 prev = ts;
588 }
589 }
590
591 #[test]
592 fn now_nanos_concurrent_uniqueness() {
593 use std::collections::HashSet;
594 use std::sync::Arc;
595
596 let timestamps = Arc::new(parking_lot::Mutex::new(Vec::new()));
597 let handles: Vec<_> = (0..4)
598 .map(|_| {
599 let ts = Arc::clone(×tamps);
600 std::thread::spawn(move || {
601 let mut local = Vec::with_capacity(250);
602 for _ in 0..250 {
603 local.push(now_nanos());
604 }
605 ts.lock().extend(local);
606 })
607 })
608 .collect();
609 for h in handles {
610 h.join().unwrap();
611 }
612 let all = timestamps.lock();
613 let unique: HashSet<_> = all.iter().copied().collect();
614 assert_eq!(
615 unique.len(),
616 all.len(),
617 "all {} timestamps must be unique, got {} unique",
618 all.len(),
619 unique.len()
620 );
621 }
622}