Skip to main content

immutable_logging/
log_entry.rs

1//! Log Entry - Structure of immutable audit log entries
2
3use serde::{Deserialize, Serialize};
4use sha2::{Sha256, Digest};
5use chrono::Utc;
6
7/// Event types for audit logging
8#[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/// Actor (user/system) information
32#[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/// Request context
41#[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/// Compliance information
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Compliance {
53    pub legal_basis: String,
54    pub retention_years: u32,
55}
56
57/// Integrity metadata
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Integrity {
60    pub content_hash: String,
61    pub previous_entry_hash: String,
62}
63
64/// Log entry structure
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct LogEntry {
67    /// Unique entry ID (format: le_{timestamp}_{uuid})
68    pub entry_id: String,
69    /// Schema version
70    pub version: String,
71    /// Unix timestamp
72    pub timestamp_unix: i64,
73    /// ISO 8601 timestamp
74    pub timestamp_iso: String,
75    /// Event type
76    pub event_type: EventType,
77    /// Actor information
78    pub actor: Actor,
79    /// Request context
80    pub request: Option<RequestContext>,
81    /// Compliance metadata
82    pub compliance: Option<Compliance>,
83    /// Integrity metadata
84    pub integrity: Integrity,
85    /// Decision made
86    pub decision: Decision,
87    /// Rule ID that triggered decision (if any)
88    pub rule_id: Option<String>,
89}
90
91/// Access decision
92#[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    /// Create a new log entry
109    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, &timestamp_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(), // Will be set by chain
143            },
144            decision: Decision::Allow,
145            rule_id: None,
146        }
147    }
148    
149    /// Compute content hash
150    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    /// Compute full hash including previous entry
168    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    /// Update previous hash (called when appending to chain)
179    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); // SHA-256 hex
212    }
213}