1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AuditEntry {
11 pub id: i64,
13 pub timestamp: String,
15 pub event_type: AuditEventType,
17 pub subject: String,
19 pub details: serde_json::Value,
21 pub operator: String,
23 pub entry_hash: String,
25 pub prev_hash: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum AuditEventType {
33 PolicyEvaluation,
34 Promotion,
35 InstallationDetected,
36 DriftDetected,
37 ViolationResolved,
38 OperatorAction,
39 GateExecution,
40 ConfigChange,
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 fn make_entry() -> AuditEntry {
48 AuditEntry {
49 id: 1,
50 timestamp: "2025-01-01T00:00:00.000Z".to_string(),
51 event_type: AuditEventType::PolicyEvaluation,
52 subject: "prod-workspace".to_string(),
53 details: serde_json::json!({"rule": "require-security-scanner", "passed": false}),
54 operator: "headless".to_string(),
55 entry_hash: "abc123".to_string(),
56 prev_hash: "".to_string(),
57 }
58 }
59
60 #[test]
61 fn audit_entry_round_trip() {
62 let entry = make_entry();
63 let json = serde_json::to_string(&entry).expect("serialize");
64 let decoded: AuditEntry = serde_json::from_str(&json).expect("deserialize");
65 assert_eq!(decoded.id, 1);
66 assert_eq!(decoded.timestamp, "2025-01-01T00:00:00.000Z");
67 assert_eq!(decoded.event_type, AuditEventType::PolicyEvaluation);
68 assert_eq!(decoded.subject, "prod-workspace");
69 assert_eq!(decoded.operator, "headless");
70 assert_eq!(decoded.entry_hash, "abc123");
71 assert_eq!(decoded.prev_hash, "");
72 }
73
74 #[test]
75 fn audit_event_type_round_trip() {
76 let variants = [
77 AuditEventType::PolicyEvaluation,
78 AuditEventType::Promotion,
79 AuditEventType::InstallationDetected,
80 AuditEventType::DriftDetected,
81 AuditEventType::ViolationResolved,
82 AuditEventType::OperatorAction,
83 AuditEventType::GateExecution,
84 AuditEventType::ConfigChange,
85 ];
86 for variant in &variants {
87 let json = serde_json::to_string(variant).expect("serialize");
88 let decoded: AuditEventType = serde_json::from_str(&json).expect("deserialize");
89 assert_eq!(&decoded, variant);
90 }
91 }
92
93 #[test]
94 fn audit_event_type_serialized_strings() {
95 assert_eq!(
96 serde_json::to_string(&AuditEventType::PolicyEvaluation).unwrap(),
97 "\"policy_evaluation\""
98 );
99 assert_eq!(
100 serde_json::to_string(&AuditEventType::Promotion).unwrap(),
101 "\"promotion\""
102 );
103 assert_eq!(
104 serde_json::to_string(&AuditEventType::InstallationDetected).unwrap(),
105 "\"installation_detected\""
106 );
107 assert_eq!(
108 serde_json::to_string(&AuditEventType::DriftDetected).unwrap(),
109 "\"drift_detected\""
110 );
111 assert_eq!(
112 serde_json::to_string(&AuditEventType::ViolationResolved).unwrap(),
113 "\"violation_resolved\""
114 );
115 assert_eq!(
116 serde_json::to_string(&AuditEventType::OperatorAction).unwrap(),
117 "\"operator_action\""
118 );
119 assert_eq!(
120 serde_json::to_string(&AuditEventType::GateExecution).unwrap(),
121 "\"gate_execution\""
122 );
123 assert_eq!(
124 serde_json::to_string(&AuditEventType::ConfigChange).unwrap(),
125 "\"config_change\""
126 );
127 }
128
129 #[test]
130 fn audit_entry_details_is_arbitrary_json() {
131 let entry = AuditEntry {
132 id: 42,
133 timestamp: "2025-06-01T12:00:00.000Z".to_string(),
134 event_type: AuditEventType::GateExecution,
135 subject: "validate:lint".to_string(),
136 details: serde_json::json!({
137 "gate": "validate:lint",
138 "exit_code": 0,
139 "duration_ms": 1500,
140 }),
141 operator: "system".to_string(),
142 entry_hash: "deadbeef".to_string(),
143 prev_hash: "cafebabe".to_string(),
144 };
145 let json = serde_json::to_string(&entry).unwrap();
146 let decoded: AuditEntry = serde_json::from_str(&json).unwrap();
147 assert_eq!(decoded.details["gate"], "validate:lint");
148 assert_eq!(decoded.details["exit_code"], 0);
149 }
150}