1use serde::{Deserialize, Serialize};
4use sha2::{Sha256, Digest};
5use chrono::Utc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "UPPERCASE")]
10pub enum EventType {
11 AccountQuery,
12 AuthSuccess,
13 AuthFailure,
14 SessionStart,
15 SessionEnd,
16 RuleViolation,
17 AnomalyDetected,
18 TokenRevoked,
19 MissionCreated,
20 MissionExpired,
21 ExportRequested,
22 DataAccess,
23}
24
25impl Default for EventType {
26 fn default() -> Self {
27 EventType::DataAccess
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Actor {
34 pub agent_id: String,
35 pub agent_org: String,
36 pub mission_id: Option<String>,
37 pub mission_type: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct RequestContext {
43 pub query_type: Option<String>,
44 pub justification: Option<String>,
45 pub result_count: Option<u32>,
46 pub ip_address: Option<String>,
47 pub user_agent: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Compliance {
53 pub legal_basis: String,
54 pub retention_years: u32,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Integrity {
60 pub content_hash: String,
61 pub previous_entry_hash: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct LogEntry {
67 pub entry_id: String,
69 pub version: String,
71 pub timestamp_unix: i64,
73 pub timestamp_iso: String,
75 pub event_type: EventType,
77 pub actor: Actor,
79 pub request: Option<RequestContext>,
81 pub compliance: Option<Compliance>,
83 pub integrity: Integrity,
85 pub decision: Decision,
87 pub rule_id: Option<String>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "UPPERCASE")]
94pub enum Decision {
95 Allow,
96 Block,
97 Warn,
98 ApprovalRequired,
99}
100
101impl Default for Decision {
102 fn default() -> Self {
103 Decision::Allow
104 }
105}
106
107impl LogEntry {
108 pub fn new(
110 event_type: EventType,
111 agent_id: String,
112 agent_org: String,
113 ) -> Self {
114 let timestamp = Utc::now();
115 let timestamp_unix = timestamp.timestamp();
116 let timestamp_iso = timestamp.to_rfc3339();
117
118 let entry_id = format!(
119 "le_{}_{}",
120 timestamp_unix,
121 uuid::Uuid::new_v4().to_string().split('-').next().unwrap()
122 );
123
124 let content_hash = Self::compute_content_hash(&entry_id, ×tamp_iso, event_type, &agent_id, &agent_org);
125
126 LogEntry {
127 entry_id,
128 version: "1.0".to_string(),
129 timestamp_unix,
130 timestamp_iso,
131 event_type,
132 actor: Actor {
133 agent_id,
134 agent_org,
135 mission_id: None,
136 mission_type: None,
137 },
138 request: None,
139 compliance: None,
140 integrity: Integrity {
141 content_hash: content_hash.clone(),
142 previous_entry_hash: String::new(), },
144 decision: Decision::Allow,
145 rule_id: None,
146 }
147 }
148
149 fn compute_content_hash(
151 entry_id: &str,
152 timestamp: &str,
153 event_type: EventType,
154 agent_id: &str,
155 agent_org: &str,
156 ) -> String {
157 let mut hasher = Sha256::new();
158 hasher.update(entry_id.as_bytes());
159 hasher.update(timestamp.as_bytes());
160 hasher.update(format!("{:?}", event_type).as_bytes());
161 hasher.update(agent_id.as_bytes());
162 hasher.update(agent_org.as_bytes());
163
164 format!("{:x}", hasher.finalize())
165 }
166
167 pub fn compute_hash(&self, previous_hash: &str) -> String {
169 let mut hasher = Sha256::new();
170 hasher.update(self.entry_id.as_bytes());
171 hasher.update(self.timestamp_iso.as_bytes());
172 hasher.update(self.integrity.content_hash.as_bytes());
173 hasher.update(previous_hash.as_bytes());
174
175 format!("{:x}", hasher.finalize())
176 }
177
178 pub fn update_previous_hash(&mut self, previous_hash: &str) {
180 self.integrity.previous_entry_hash = previous_hash.to_string();
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_create_entry() {
190 let entry = LogEntry::new(
191 EventType::AccountQuery,
192 "AGENT_001".to_string(),
193 "FISCALITE_DGFiP".to_string(),
194 );
195
196 assert!(entry.entry_id.starts_with("le_"));
197 assert_eq!(entry.event_type, EventType::AccountQuery);
198 }
199
200 #[test]
201 fn test_compute_hash() {
202 let mut entry = LogEntry::new(
203 EventType::AuthSuccess,
204 "AGENT_001".to_string(),
205 "GENDARMERIE".to_string(),
206 );
207
208 entry.update_previous_hash("previous_hash_123");
209 let hash = entry.compute_hash("previous_hash_123");
210
211 assert_eq!(hash.len(), 64); }
213}