feroxbuster/
message.rs

1use anyhow::Context;
2use console::{style, Color};
3use serde::{Deserialize, Serialize};
4
5use crate::traits::FeroxSerialize;
6use crate::utils::fmt_err;
7
8#[derive(Serialize, Deserialize, Default, Debug)]
9/// Representation of a log entry, can be represented as a human readable string or JSON
10pub struct FeroxMessage {
11    #[serde(rename = "type")]
12    /// Name of this type of struct, used for serialization, i.e. `{"type":"log"}`
13    pub(crate) kind: String,
14
15    /// The log message
16    pub(crate) message: String,
17
18    /// The log level
19    pub(crate) level: String,
20
21    /// The number of seconds elapsed since the scan started
22    pub(crate) time_offset: f32,
23
24    /// The module from which log::* was called
25    pub(crate) module: String,
26}
27
28/// Implementation of FeroxMessage
29impl FeroxSerialize for FeroxMessage {
30    /// Create a string representation of the log message
31    ///
32    /// ex:  301       10l       16w      173c https://localhost/api
33    fn as_str(&self) -> String {
34        let (level_name, level_color) = match self.level.as_str() {
35            "ERROR" => ("ERR", Color::Red),
36            "WARN" => ("WRN", Color::Red),
37            "INFO" => ("INF", Color::Cyan),
38            "DEBUG" => ("DBG", Color::Yellow),
39            "TRACE" => ("TRC", Color::Magenta),
40            "WILDCARD" => ("WLD", Color::Cyan),
41            _ => ("MSG", Color::White),
42        };
43
44        format!(
45            "{} {:10.03} {} {}\n",
46            style(level_name).bg(level_color).black(),
47            style(self.time_offset).dim(),
48            self.module,
49            style(&self.message).dim(),
50        )
51    }
52
53    /// Create an NDJSON representation of the log message
54    ///
55    /// (expanded for clarity)
56    /// ex:
57    /// {
58    ///   "type": "log",
59    ///   "message": "Sent https://localhost/api to file handler",
60    ///   "level": "DEBUG",
61    ///   "time_offset": 0.86333454,
62    ///   "module": "feroxbuster::reporter"
63    /// }\n
64    fn as_json(&self) -> anyhow::Result<String> {
65        let mut json = serde_json::to_string(&self).with_context(|| {
66            fmt_err(&format!(
67                "Could not convert {}:{} to JSON",
68                self.level, self.message
69            ))
70        })?;
71        json.push('\n');
72        Ok(json)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    /// test as_str method of FeroxMessage
82    fn ferox_message_as_str_returns_string_with_newline() {
83        let message = FeroxMessage {
84            message: "message".to_string(),
85            module: "utils".to_string(),
86            time_offset: 1.0,
87            level: "INFO".to_string(),
88            kind: "log".to_string(),
89        };
90        let message_str = message.as_str();
91
92        assert!(message_str.contains("INF"));
93        assert!(message_str.contains("1.000"));
94        assert!(message_str.contains("utils"));
95        assert!(message_str.contains("message"));
96        assert!(message_str.ends_with('\n'));
97    }
98
99    #[test]
100    /// test as_json method of FeroxMessage
101    fn ferox_message_as_json_returns_json_representation_of_ferox_message_with_newline() {
102        let message = FeroxMessage {
103            message: "message".to_string(),
104            module: "utils".to_string(),
105            time_offset: 1.0,
106            level: "INFO".to_string(),
107            kind: "log".to_string(),
108        };
109
110        let message_str = message.as_json().unwrap();
111
112        let error_margin = f32::EPSILON;
113
114        let json: FeroxMessage = serde_json::from_str(&message_str).unwrap();
115        assert_eq!(json.module, message.module);
116        assert_eq!(json.message, message.message);
117        assert!((json.time_offset - message.time_offset).abs() < error_margin);
118        assert_eq!(json.level, message.level);
119        assert_eq!(json.kind, message.kind);
120    }
121
122    #[test]
123    /// test defaults for coverage
124    fn message_defaults() {
125        let msg = FeroxMessage::default();
126        assert_eq!(msg.level, String::new());
127        assert_eq!(msg.kind, String::new());
128        assert_eq!(msg.message, String::new());
129        assert_eq!(msg.module, String::new());
130        assert!(msg.time_offset < 0.1);
131    }
132
133    #[test]
134    /// ensure WILDCARD messages serialize to WLD and anything not known to UNK
135    fn message_as_str_edges() {
136        let mut msg = FeroxMessage {
137            message: "message".to_string(),
138            module: "utils".to_string(),
139            time_offset: 1.0,
140            level: "WILDCARD".to_string(),
141            kind: "log".to_string(),
142        };
143        assert!(console::strip_ansi_codes(&msg.as_str()).starts_with("WLD"));
144
145        msg.level = "UNKNOWN".to_string();
146        assert!(console::strip_ansi_codes(&msg.as_str()).starts_with("MSG"));
147    }
148}