rust_secure_logger/
entry.rs

1//! Log entry structure with cryptographic integrity
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7/// Security levels for log entries
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9pub enum SecurityLevel {
10    /// Informational message
11    Info,
12    /// Warning that may require attention
13    Warning,
14    /// Security event requiring review
15    SecurityEvent,
16    /// Critical security incident
17    Critical,
18    /// Audit trail entry (financial transactions, access control)
19    Audit,
20}
21
22impl SecurityLevel {
23    /// Get string representation
24    pub fn as_str(&self) -> &'static str {
25        match self {
26            SecurityLevel::Info => "INFO",
27            SecurityLevel::Warning => "WARNING",
28            SecurityLevel::SecurityEvent => "SECURITY_EVENT",
29            SecurityLevel::Critical => "CRITICAL",
30            SecurityLevel::Audit => "AUDIT",
31        }
32    }
33
34    /// Get numeric severity (for SIEM integration)
35    pub fn severity(&self) -> u8 {
36        match self {
37            SecurityLevel::Info => 1,
38            SecurityLevel::Warning => 3,
39            SecurityLevel::SecurityEvent => 5,
40            SecurityLevel::Critical => 8,
41            SecurityLevel::Audit => 6,
42        }
43    }
44}
45
46/// A single log entry with cryptographic integrity protection
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct LogEntry {
49    /// UTC timestamp when entry was created
50    pub timestamp: DateTime<Utc>,
51    /// Security level
52    pub level: SecurityLevel,
53    /// Log message
54    pub message: String,
55    /// Optional structured data (user ID, transaction ID, etc.)
56    pub metadata: Option<serde_json::Value>,
57    /// SHA-256 hash of entry content for tamper detection
58    pub integrity_hash: String,
59    /// Optional source identifier (hostname, service name)
60    pub source: Option<String>,
61    /// Optional category for filtering
62    pub category: Option<String>,
63}
64
65impl LogEntry {
66    /// Create a new log entry with integrity hash
67    pub fn new(level: SecurityLevel, message: String, metadata: Option<serde_json::Value>) -> Self {
68        let timestamp = Utc::now();
69        let mut entry = Self {
70            timestamp,
71            level,
72            message,
73            metadata,
74            integrity_hash: String::new(),
75            source: None,
76            category: None,
77        };
78        entry.integrity_hash = entry.calculate_hash();
79        entry
80    }
81
82    /// Create a new log entry with source and category
83    pub fn new_with_context(
84        level: SecurityLevel,
85        message: String,
86        metadata: Option<serde_json::Value>,
87        source: Option<String>,
88        category: Option<String>,
89    ) -> Self {
90        let timestamp = Utc::now();
91        let mut entry = Self {
92            timestamp,
93            level,
94            message,
95            metadata,
96            integrity_hash: String::new(),
97            source,
98            category,
99        };
100        entry.integrity_hash = entry.calculate_hash();
101        entry
102    }
103
104    /// Calculate cryptographic hash of entry content
105    fn calculate_hash(&self) -> String {
106        let mut hasher = Sha256::new();
107        hasher.update(self.timestamp.to_rfc3339().as_bytes());
108        hasher.update(format!("{:?}", self.level).as_bytes());
109        hasher.update(self.message.as_bytes());
110        if let Some(ref meta) = self.metadata {
111            hasher.update(meta.to_string().as_bytes());
112        }
113        if let Some(ref source) = self.source {
114            hasher.update(source.as_bytes());
115        }
116        if let Some(ref category) = self.category {
117            hasher.update(category.as_bytes());
118        }
119        format!("{:x}", hasher.finalize())
120    }
121
122    /// Verify entry integrity (detect tampering)
123    pub fn verify_integrity(&self) -> bool {
124        let calculated = self.calculate_hash();
125        calculated == self.integrity_hash
126    }
127
128    /// Serialize to JSON string
129    pub fn to_json(&self) -> Result<String, serde_json::Error> {
130        serde_json::to_string(self)
131    }
132
133    /// Serialize to pretty JSON
134    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
135        serde_json::to_string_pretty(self)
136    }
137
138    /// Get formatted log line for file output
139    pub fn to_log_line(&self) -> String {
140        format!(
141            "[{}] [{}] {} {}",
142            self.timestamp.to_rfc3339(),
143            self.level.as_str(),
144            self.message,
145            if let Some(ref meta) = self.metadata {
146                format!("| metadata: {}", meta)
147            } else {
148                String::new()
149            }
150        )
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_security_level_string() {
160        assert_eq!(SecurityLevel::Info.as_str(), "INFO");
161        assert_eq!(SecurityLevel::Critical.as_str(), "CRITICAL");
162    }
163
164    #[test]
165    fn test_security_level_severity() {
166        assert_eq!(SecurityLevel::Info.severity(), 1);
167        assert_eq!(SecurityLevel::Critical.severity(), 8);
168    }
169
170    #[test]
171    fn test_log_entry_creation() {
172        let entry = LogEntry::new(SecurityLevel::Info, "Test message".to_string(), None);
173        assert_eq!(entry.level, SecurityLevel::Info);
174        assert_eq!(entry.message, "Test message");
175        assert!(!entry.integrity_hash.is_empty());
176    }
177
178    #[test]
179    fn test_integrity_verification() {
180        let entry = LogEntry::new(SecurityLevel::Audit, "Transaction: $1000".to_string(), None);
181        assert!(entry.verify_integrity());
182    }
183
184    #[test]
185    fn test_tampering_detection() {
186        let mut entry = LogEntry::new(SecurityLevel::Audit, "Original message".to_string(), None);
187        entry.message = "Tampered message".to_string();
188        assert!(!entry.verify_integrity());
189    }
190
191    #[test]
192    fn test_entry_with_context() {
193        let entry = LogEntry::new_with_context(
194            SecurityLevel::SecurityEvent,
195            "Login failed".to_string(),
196            None,
197            Some("web-server-01".to_string()),
198            Some("authentication".to_string()),
199        );
200        assert_eq!(entry.source, Some("web-server-01".to_string()));
201        assert_eq!(entry.category, Some("authentication".to_string()));
202        assert!(entry.verify_integrity());
203    }
204
205    #[test]
206    fn test_to_log_line() {
207        let entry = LogEntry::new(SecurityLevel::Warning, "High CPU usage".to_string(), None);
208        let log_line = entry.to_log_line();
209        assert!(log_line.contains("WARNING"));
210        assert!(log_line.contains("High CPU usage"));
211    }
212}