rust_secure_logger/
formats.rs

1//! SIEM export formats (CEF, LEEF, Syslog)
2
3use crate::entry::{LogEntry, SecurityLevel};
4use chrono::Utc;
5
6/// Common Event Format (CEF) - ArcSight standard
7pub struct CEFFormatter;
8
9impl CEFFormatter {
10    /// Convert log entry to CEF format
11    /// Format: CEF:Version|Device Vendor|Device Product|Device Version|Device Event Class ID|Name|Severity|Extension
12    pub fn format(entry: &LogEntry) -> String {
13        let device_vendor = "GuardsArm";
14        let device_product = "SecureLogger";
15        let device_version = "1.0";
16        let event_class_id = Self::get_event_class_id(&entry.level);
17        let name = &entry.message;
18        let severity = entry.level.severity();
19
20        // Build extension fields
21        let mut extensions = Vec::new();
22        extensions.push(format!("rt={}", entry.timestamp.timestamp_millis()));
23
24        if let Some(ref source) = entry.source {
25            extensions.push(format!("shost={}", source));
26        }
27
28        if let Some(ref category) = entry.category {
29            extensions.push(format!("cat={}", category));
30        }
31
32        if let Some(ref meta) = entry.metadata {
33            extensions.push(format!("cs1={}", meta.to_string()));
34            extensions.push("cs1Label=metadata".to_string());
35        }
36
37        extensions.push(format!("msg={}", entry.message));
38
39        format!(
40            "CEF:0|{}|{}|{}|{}|{}|{}|{}",
41            device_vendor,
42            device_product,
43            device_version,
44            event_class_id,
45            name,
46            severity,
47            extensions.join(" ")
48        )
49    }
50
51    fn get_event_class_id(level: &SecurityLevel) -> &'static str {
52        match level {
53            SecurityLevel::Info => "INFO-001",
54            SecurityLevel::Warning => "WARN-002",
55            SecurityLevel::SecurityEvent => "SEC-003",
56            SecurityLevel::Critical => "CRIT-004",
57            SecurityLevel::Audit => "AUDIT-005",
58        }
59    }
60}
61
62/// Log Event Extended Format (LEEF) - IBM QRadar standard
63pub struct LEEFFormatter;
64
65impl LEEFFormatter {
66    /// Convert log entry to LEEF format
67    /// Format: LEEF:Version|Vendor|Product|Version|EventID|Delimiter|Key=Value pairs
68    pub fn format(entry: &LogEntry) -> String {
69        let vendor = "GuardsArm";
70        let product = "SecureLogger";
71        let version = "1.0";
72        let event_id = Self::get_event_id(&entry.level);
73        let delimiter = "\t";
74
75        let mut fields = Vec::new();
76        fields.push(format!("devTime={}", entry.timestamp.to_rfc3339()));
77        fields.push(format!("severity={}", entry.level.severity()));
78        fields.push(format!("cat={}", entry.level.as_str()));
79        fields.push(format!("msg={}", entry.message));
80
81        if let Some(ref source) = entry.source {
82            fields.push(format!("src={}", source));
83        }
84
85        if let Some(ref category) = entry.category {
86            fields.push(format!("eventCategory={}", category));
87        }
88
89        if let Some(ref meta) = entry.metadata {
90            fields.push(format!("usrName={}", meta.to_string()));
91        }
92
93        format!(
94            "LEEF:2.0|{}|{}|{}|{}|{}|{}",
95            vendor,
96            product,
97            version,
98            event_id,
99            delimiter,
100            fields.join(delimiter)
101        )
102    }
103
104    fn get_event_id(level: &SecurityLevel) -> &'static str {
105        match level {
106            SecurityLevel::Info => "1000",
107            SecurityLevel::Warning => "2000",
108            SecurityLevel::SecurityEvent => "3000",
109            SecurityLevel::Critical => "4000",
110            SecurityLevel::Audit => "5000",
111        }
112    }
113}
114
115/// Syslog RFC 5424 format
116pub struct SyslogFormatter;
117
118impl SyslogFormatter {
119    /// Convert log entry to Syslog RFC 5424 format
120    pub fn format(entry: &LogEntry) -> String {
121        let priority = Self::calculate_priority(&entry.level);
122        let version = 1;
123        let timestamp = entry.timestamp.to_rfc3339();
124        let hostname = entry.source.as_deref().unwrap_or("-");
125        let app_name = "SecureLogger";
126        let proc_id = std::process::id();
127        let msg_id = entry.level.as_str();
128
129        // Structured data
130        let structured_data = if let Some(ref meta) = entry.metadata {
131            format!("[metadata@32473 data=\"{}\"]", meta.to_string().replace('"', "\\\""))
132        } else {
133            "-".to_string()
134        };
135
136        format!(
137            "<{}>{} {} {} {} {} {} {} {}",
138            priority,
139            version,
140            timestamp,
141            hostname,
142            app_name,
143            proc_id,
144            msg_id,
145            structured_data,
146            entry.message
147        )
148    }
149
150    fn calculate_priority(level: &SecurityLevel) -> u8 {
151        // Facility: 16 (local use 0)
152        // Severity mapping
153        let severity = match level {
154            SecurityLevel::Info => 6,       // Informational
155            SecurityLevel::Warning => 4,    // Warning
156            SecurityLevel::SecurityEvent => 2, // Critical
157            SecurityLevel::Critical => 1,   // Alert
158            SecurityLevel::Audit => 5,      // Notice
159        };
160        (16 * 8) + severity
161    }
162}
163
164/// Splunk HEC (HTTP Event Collector) format
165pub struct SplunkFormatter;
166
167impl SplunkFormatter {
168    /// Convert log entry to Splunk HEC JSON format
169    pub fn format(entry: &LogEntry) -> String {
170        let event = serde_json::json!({
171            "time": entry.timestamp.timestamp(),
172            "host": entry.source.as_ref().unwrap_or(&"unknown".to_string()),
173            "source": "secure_logger",
174            "sourcetype": "_json",
175            "event": {
176                "level": entry.level.as_str(),
177                "message": entry.message,
178                "severity": entry.level.severity(),
179                "category": entry.category,
180                "metadata": entry.metadata,
181                "integrity_hash": entry.integrity_hash,
182            }
183        });
184
185        event.to_string()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn create_test_entry() -> LogEntry {
194        LogEntry::new_with_context(
195            SecurityLevel::SecurityEvent,
196            "Failed login attempt".to_string(),
197            Some(serde_json::json!({"username": "admin", "ip": "192.168.1.100"})),
198            Some("web-server-01".to_string()),
199            Some("authentication".to_string()),
200        )
201    }
202
203    #[test]
204    fn test_cef_format() {
205        let entry = create_test_entry();
206        let cef = CEFFormatter::format(&entry);
207
208        assert!(cef.starts_with("CEF:0|"));
209        assert!(cef.contains("GuardsArm"));
210        assert!(cef.contains("SecureLogger"));
211        assert!(cef.contains("SEC-003"));
212        assert!(cef.contains("Failed login attempt"));
213    }
214
215    #[test]
216    fn test_leef_format() {
217        let entry = create_test_entry();
218        let leef = LEEFFormatter::format(&entry);
219
220        assert!(leef.starts_with("LEEF:2.0|"));
221        assert!(leef.contains("GuardsArm"));
222        assert!(leef.contains("SecureLogger"));
223        assert!(leef.contains("3000"));
224        assert!(leef.contains("Failed login attempt"));
225    }
226
227    #[test]
228    fn test_syslog_format() {
229        let entry = create_test_entry();
230        let syslog = SyslogFormatter::format(&entry);
231
232        assert!(syslog.starts_with("<130>1")); // Priority 130, version 1
233        assert!(syslog.contains("SecureLogger"));
234        assert!(syslog.contains("Failed login attempt"));
235    }
236
237    #[test]
238    fn test_splunk_format() {
239        let entry = create_test_entry();
240        let splunk = SplunkFormatter::format(&entry);
241
242        assert!(splunk.contains("\"source\":\"secure_logger\""));
243        assert!(splunk.contains("\"level\":\"SECURITY_EVENT\""));
244        assert!(splunk.contains("Failed login attempt"));
245    }
246
247    #[test]
248    fn test_cef_severity_levels() {
249        let levels = vec![
250            (SecurityLevel::Info, "1"),
251            (SecurityLevel::Warning, "3"),
252            (SecurityLevel::SecurityEvent, "5"),
253            (SecurityLevel::Critical, "8"),
254        ];
255
256        for (level, expected_sev) in levels {
257            let entry = LogEntry::new(level, "Test".to_string(), None);
258            let cef = CEFFormatter::format(&entry);
259            assert!(cef.contains(&format!("|{}|", expected_sev)));
260        }
261    }
262
263    #[test]
264    fn test_leef_event_ids() {
265        let levels = vec![
266            (SecurityLevel::Info, "1000"),
267            (SecurityLevel::Warning, "2000"),
268            (SecurityLevel::SecurityEvent, "3000"),
269            (SecurityLevel::Critical, "4000"),
270            (SecurityLevel::Audit, "5000"),
271        ];
272
273        for (level, expected_id) in levels {
274            let entry = LogEntry::new(level, "Test".to_string(), None);
275            let leef = LEEFFormatter::format(&entry);
276            assert!(leef.contains(expected_id));
277        }
278    }
279}