rust_secure_logger/
formats.rs

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