1use crate::entry::{LogEntry, SecurityLevel};
4use chrono::Utc;
5
6pub struct CEFFormatter;
8
9impl CEFFormatter {
10 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 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
62pub struct LEEFFormatter;
64
65impl LEEFFormatter {
66 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
115pub struct SyslogFormatter;
117
118impl SyslogFormatter {
119 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 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 let severity = match level {
154 SecurityLevel::Info => 6, SecurityLevel::Warning => 4, SecurityLevel::SecurityEvent => 2, SecurityLevel::Critical => 1, SecurityLevel::Audit => 5, };
160 (16 * 8) + severity
161 }
162}
163
164pub struct SplunkFormatter;
166
167impl SplunkFormatter {
168 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")); 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}