interactsh_rs/
interaction_log.rs

1use std::fmt::Display;
2
3use serde::Deserialize;
4use time::OffsetDateTime;
5
6
7/// Type returned when a [RegisteredClient](crate::client::RegisteredClient)
8/// polls a server and obtains new interaction logs
9///
10/// Whether or not a raw log or a parsed log is
11/// returned depends on the following:
12/// 1. If the client was built with the "parse logs" option set to true
13/// (see [ClientBuilder](crate::client::ClientBuilder))
14/// 2. If the logs are able to be parsed (if the logs are unable to be parsed, then the raw
15/// logs are returned)
16#[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/// Wrapper type containing the raw log string received by the client from the
42/// Interactsh server (after decoding and decrypting)
43#[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/// A fully parsed log entry returned by an Interactsh server
76#[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}