1use std::fmt::Display;
2
3use serde::Deserialize;
4use time::OffsetDateTime;
5
6
7#[derive(Debug)]
17pub enum LogEntry {
18 ParsedLog(ParsedLogEntry),
19 RawLog(RawLog),
20}
21
22impl LogEntry {
23 #[allow(dead_code)]
24 pub(crate) fn return_raw_log(raw_log_str: &str) -> LogEntry {
25 let raw_log = RawLog {
26 log_entry: raw_log_str.to_owned(),
27 };
28
29 Self::RawLog(raw_log)
30 }
31
32 #[allow(dead_code)]
33 pub(crate) fn try_parse_log(raw_log_str: &str) -> LogEntry {
34 match serde_json::from_str::<ParsedLogEntry>(raw_log_str) {
35 Ok(parsed_log) => Self::ParsedLog(parsed_log),
36 Err(_) => Self::return_raw_log(raw_log_str),
37 }
38 }
39}
40
41#[derive(Debug)]
44pub struct RawLog {
45 pub log_entry: String,
46}
47
48#[derive(Debug, Deserialize)]
49pub enum DnsQType {
50 A,
51 NS,
52 CNAME,
53 SOA,
54 PTR,
55 MX,
56 TXT,
57 AAAA,
58}
59
60impl Display for DnsQType {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 DnsQType::A => write!(f, "A"),
64 DnsQType::NS => write!(f, "NS"),
65 DnsQType::CNAME => write!(f, "CNAME"),
66 DnsQType::SOA => write!(f, "SOA"),
67 DnsQType::PTR => write!(f, "PTR"),
68 DnsQType::MX => write!(f, "MX"),
69 DnsQType::TXT => write!(f, "TXT"),
70 DnsQType::AAAA => write!(f, "AAAA"),
71 }
72 }
73}
74
75#[derive(Debug, Deserialize)]
77#[serde(tag = "protocol")]
78pub enum ParsedLogEntry {
79 #[serde(alias = "dns", rename_all(deserialize = "kebab-case"))]
80 Dns {
81 unique_id: String,
82 full_id: String,
83 q_type: Option<DnsQType>,
84 raw_request: String,
85 raw_response: String,
86 remote_address: std::net::IpAddr,
87 #[serde(with = "timestamp_unixstr_parse")]
88 timestamp: OffsetDateTime,
89 },
90
91 #[serde(alias = "ftp", rename_all(deserialize = "kebab-case"))]
92 Ftp {
93 remote_address: std::net::IpAddr,
94 raw_request: String,
95 #[serde(with = "timestamp_unixstr_parse")]
96 timestamp: OffsetDateTime,
97 },
98
99 #[serde(alias = "http", rename_all(deserialize = "kebab-case"))]
100 Http {
101 unique_id: String,
102 full_id: String,
103 raw_request: String,
104 raw_response: String,
105 remote_address: std::net::IpAddr,
106 #[serde(with = "timestamp_unixstr_parse")]
107 timestamp: OffsetDateTime,
108 },
109
110 #[serde(alias = "ldap", rename_all(deserialize = "kebab-case"))]
111 Ldap {
112 unique_id: String,
113 full_id: String,
114 raw_request: String,
115 raw_response: String,
116 remote_address: std::net::IpAddr,
117 #[serde(with = "timestamp_unixstr_parse")]
118 timestamp: OffsetDateTime,
119 },
120
121 #[serde(alias = "smb", rename_all(deserialize = "kebab-case"))]
122 Smb {
123 raw_request: String,
124 #[serde(with = "timestamp_unixstr_parse")]
125 timestamp: OffsetDateTime,
126 },
127
128 #[serde(alias = "smtp", rename_all(deserialize = "kebab-case"))]
129 Smtp {
130 unique_id: String,
131 full_id: String,
132 raw_request: String,
133 smtp_from: String,
134 remote_address: std::net::IpAddr,
135 #[serde(with = "timestamp_unixstr_parse")]
136 timestamp: OffsetDateTime,
137 },
138}
139
140
141mod timestamp_unixstr_parse {
142 use serde::{de, Deserialize, Deserializer};
143 use time::format_description::well_known::Rfc3339;
144 use time::OffsetDateTime;
145
146 pub fn deserialize<'a, D: Deserializer<'a>>(
147 deserializer: D,
148 ) -> Result<OffsetDateTime, D::Error> {
149 OffsetDateTime::parse(<_>::deserialize(deserializer)?, &Rfc3339)
150 .map_err(|e| de::Error::custom(format!("{}", e)))
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use fake::{faker, Fake};
157 use rand::distributions::{Alphanumeric, DistString, Slice};
158 use rand::{thread_rng, Rng};
159 use serde_json::{json, Value};
160 use time::format_description::well_known::Rfc3339;
161 use time::OffsetDateTime;
162
163 use super::*;
164
165 fn get_random_id() -> String {
166 let random_id = Alphanumeric
167 .sample_string(&mut thread_rng(), 33)
168 .to_ascii_lowercase();
169
170 random_id
171 }
172
173 fn get_timestamp() -> String {
174 OffsetDateTime::now_utc().format(&Rfc3339).unwrap()
175 }
176
177 fn get_ip_address() -> String {
178 faker::internet::en::IP().fake()
179 }
180
181 fn get_paragraph() -> String {
182 faker::lorem::en::Paragraph(1..2).fake()
183 }
184
185 fn get_email_address() -> String {
186 faker::internet::en::SafeEmail().fake()
187 }
188
189 fn get_random_dns_q_type() -> String {
190 let mut rng = rand::thread_rng();
191 let q_types = ["A", "NS", "CNAME", "SOA", "PTR", "MX", "TXT", "AAAA"];
192 let q_types_dist = Slice::new(&q_types).unwrap();
193
194 rng.sample(q_types_dist).to_string()
195 }
196
197 fn try_parse_json(json_value: Value) -> LogEntry {
198 let json_value_string =
199 serde_json::to_string(&json_value).expect("Unable to parse json to string");
200 LogEntry::try_parse_log(&json_value_string)
201 }
202
203 fn get_raw_log(json_value: Value) -> LogEntry {
204 let json_value_string =
205 serde_json::to_string(&json_value).expect("Unable to parse json to string");
206 LogEntry::return_raw_log(&json_value_string)
207 }
208
209 #[test]
210 fn log_entry_successfully_parses_valid_dns_log_no_qtype() {
211 let random_id = get_random_id();
212 let timestamp = get_timestamp();
213 let remote_address = get_ip_address();
214 let raw_request = get_paragraph();
215 let raw_response = get_paragraph();
216
217 let json_log = json!({
218 "protocol": "dns",
219 "unique-id": random_id,
220 "full-id": random_id,
221 "raw-request": raw_request,
222 "raw-response": raw_response,
223 "remote-address": remote_address,
224 "timestamp": timestamp
225 });
226
227 let log_parse_result = try_parse_json(json_log);
228
229 match log_parse_result {
230 LogEntry::ParsedLog(parsed_log) => {
231 match parsed_log {
232 ParsedLogEntry::Dns { .. } => {}
233 _ => panic!("DNS log did not parse to DNS variant"),
234 }
235 }
236 LogEntry::RawLog(_) => panic!("DNS log did not parse at all"),
237 }
238 }
239
240 #[test]
241 fn log_entry_successfully_parses_valid_dns_log_with_qtype() {
242 let random_id = get_random_id();
243 let timestamp = get_timestamp();
244 let remote_address = get_ip_address();
245 let raw_request = get_paragraph();
246 let raw_response = get_paragraph();
247 let q_type = get_random_dns_q_type();
248
249 let json_log = json!({
250 "protocol": "dns",
251 "unique-id": random_id,
252 "full-id": random_id,
253 "q-type": q_type,
254 "raw-request": raw_request,
255 "raw-response": raw_response,
256 "remote-address": remote_address,
257 "timestamp": timestamp
258 });
259
260 let log_parse_result = try_parse_json(json_log);
261
262 match log_parse_result {
263 LogEntry::ParsedLog(parsed_log) => {
264 match parsed_log {
265 ParsedLogEntry::Dns { .. } => {}
266 _ => panic!("DNS log did not parse to DNS variant"),
267 }
268 }
269 LogEntry::RawLog(_) => panic!("DNS log did not parse at all"),
270 }
271 }
272
273 #[test]
274 fn log_entry_successfully_parses_valid_http_log() {
275 let random_id = get_random_id();
276 let timestamp = get_timestamp();
277 let remote_address = get_ip_address();
278 let raw_request = get_paragraph();
279 let raw_response = get_paragraph();
280
281 let json_log = json!({
282 "protocol": "http",
283 "unique-id": random_id,
284 "full-id": random_id,
285 "raw-request": raw_request,
286 "raw-response": raw_response,
287 "remote-address": remote_address,
288 "timestamp": timestamp
289 });
290
291 let log_parse_result = try_parse_json(json_log);
292
293 match log_parse_result {
294 LogEntry::ParsedLog(parsed_log) => {
295 match parsed_log {
296 ParsedLogEntry::Http { .. } => {}
297 _ => panic!("HTTP log did not parse to HTTP variant"),
298 }
299 }
300 LogEntry::RawLog(_) => panic!("HTTP log did not parse at all"),
301 }
302 }
303
304 #[test]
305 fn log_entry_successfully_parses_valid_ftp_log() {
306 let timestamp = get_timestamp();
307 let remote_address = get_ip_address();
308 let raw_request = get_paragraph();
309
310 let json_log = json!({
311 "protocol": "ftp",
312 "raw-request": raw_request,
313 "remote-address": remote_address,
314 "timestamp": timestamp
315 });
316
317 let log_parse_result = try_parse_json(json_log);
318
319 match log_parse_result {
320 LogEntry::ParsedLog(parsed_log) => {
321 match parsed_log {
322 ParsedLogEntry::Ftp { .. } => {}
323 _ => panic!("FTP log did not parse to FTP variant"),
324 }
325 }
326 LogEntry::RawLog(_) => panic!("FTP log did not parse at all"),
327 }
328 }
329
330 #[test]
331 fn log_entry_successfully_parses_valid_ldap_log() {
332 let random_id = get_random_id();
333 let timestamp = get_timestamp();
334 let remote_address = get_ip_address();
335 let raw_request = get_paragraph();
336 let raw_response = get_paragraph();
337
338 let json_log = json!({
339 "protocol": "ldap",
340 "unique-id": random_id,
341 "full-id": random_id,
342 "raw-request": raw_request,
343 "raw-response": raw_response,
344 "remote-address": remote_address,
345 "timestamp": timestamp
346 });
347
348 let log_parse_result = try_parse_json(json_log);
349
350 match log_parse_result {
351 LogEntry::ParsedLog(parsed_log) => {
352 match parsed_log {
353 ParsedLogEntry::Ldap { .. } => {}
354 _ => panic!("LDAP log did not parse to LDAP variant"),
355 }
356 }
357 LogEntry::RawLog(_) => panic!("LDAP log did not parse at all"),
358 }
359 }
360
361 #[test]
362 fn log_entry_successfully_parses_valid_smb_log() {
363 let timestamp = get_timestamp();
364 let raw_request = get_paragraph();
365
366 let json_log = json!({
367 "protocol": "smb",
368 "raw-request": raw_request,
369 "timestamp": timestamp
370 });
371
372 let log_parse_result = try_parse_json(json_log);
373
374 match log_parse_result {
375 LogEntry::ParsedLog(parsed_log) => {
376 match parsed_log {
377 ParsedLogEntry::Smb { .. } => {}
378 _ => panic!("SMB log did not parse to SMB variant"),
379 }
380 }
381 LogEntry::RawLog(_) => panic!("SMB log did not parse at all"),
382 }
383 }
384
385 #[test]
386 fn log_entry_successfully_parses_valid_smtp_log() {
387 let random_id = get_random_id();
388 let timestamp = get_timestamp();
389 let remote_address = get_ip_address();
390 let raw_request = get_paragraph();
391 let email_address = get_email_address();
392
393 let json_log = json!({
394 "protocol": "smtp",
395 "unique-id": random_id,
396 "full-id": random_id,
397 "raw-request": raw_request,
398 "smtp-from": email_address,
399 "remote-address": remote_address,
400 "timestamp": timestamp
401 });
402
403 let log_parse_result = try_parse_json(json_log);
404
405 match log_parse_result {
406 LogEntry::ParsedLog(parsed_log) => {
407 match parsed_log {
408 ParsedLogEntry::Smtp { .. } => {}
409 _ => panic!("SMTP log did not parse to SMTP variant"),
410 }
411 }
412 LogEntry::RawLog(_) => panic!("SMTP log did not parse at all"),
413 }
414 }
415
416 #[test]
417 fn log_entry_returns_raw_log_for_invalid_log() {
418 let random_id = get_random_id();
419 let timestamp = get_timestamp();
420 let remote_address: String = get_ip_address();
421 let raw_request: String = get_paragraph();
422
423 let json_log = json!({
424 "protocol": "http",
425 "unique-id": random_id,
426 "full-id": random_id,
427 "raw-request": raw_request,
428 "remote-address": remote_address,
429 "timestamp": timestamp,
430 "unexpected-field": "unexpected field"
431 });
432
433 let log_parse_result = try_parse_json(json_log);
434
435 match log_parse_result {
436 LogEntry::ParsedLog(_) => panic!("Expected raw log, got a parsed log"),
437 LogEntry::RawLog(_) => {}
438 }
439 }
440
441 #[test]
442 fn log_entry_successfully_returns_raw_log() {
443 let random_id = get_random_id();
444 let timestamp = get_timestamp();
445 let remote_address: String = get_ip_address();
446 let raw_request: String = get_paragraph();
447 let raw_response: String = get_paragraph();
448
449 let json_log = json!({
450 "protocol": "http",
451 "unique-id": random_id,
452 "full-id": random_id,
453 "raw-request": raw_request,
454 "raw-response": raw_response,
455 "remote-address": remote_address,
456 "timestamp": timestamp
457 });
458
459 let log_entry = get_raw_log(json_log);
460
461 match log_entry {
462 LogEntry::ParsedLog(_) => panic!("Expected raw log, got a parsed log"),
463 LogEntry::RawLog(_) => {}
464 }
465 }
466}