phink_lib/cli/ui/monitor/
logs.rs

1use crate::{
2    cli::{
3        config::PFiles,
4        ui::traits::FromPath,
5    },
6    ResultOf,
7};
8use anyhow::bail;
9use ratatui::{
10    style::{
11        Color,
12        Modifier,
13        Style,
14    },
15    text::Span,
16};
17use regex::Regex;
18use std::{
19    fmt,
20    fmt::{
21        Debug,
22        Formatter,
23    },
24    fs,
25    fs::File,
26    io,
27    io::{
28        BufRead,
29        BufReader,
30        Seek,
31        SeekFrom,
32    },
33    path::PathBuf,
34    str::FromStr,
35};
36
37#[derive(Default, Debug, Clone)]
38pub struct AFLProperties {
39    pub run_time: String,
40    pub last_new_find: String,
41    pub last_saved_crash: String,
42    pub corpus_count: u32,
43    pub saved_crashes: u32,
44    pub exec_speed: u32,
45    pub stability: f64,
46}
47
48impl AFLProperties {
49    pub fn crashed(&self) -> bool {
50        self.saved_crashes > 0
51    }
52
53    pub fn bad_stability(&self) -> bool {
54        self.stability < 0.8
55    }
56
57    pub fn span_if_crash(&self) -> Span {
58        let crashes = &self.saved_crashes;
59        if crashes > &0 {
60            Span::styled(
61                format!("{} (you have some crashes, please check!!)", crashes),
62                Style::default()
63                    .add_modifier(Modifier::BOLD)
64                    .add_modifier(Modifier::UNDERLINED)
65                    .underline_color(Color::White)
66                    .fg(Color::Green),
67            )
68        } else {
69            Span::styled(
70                crashes.to_string(),
71                Style::default().add_modifier(Modifier::BOLD),
72            )
73        }
74    }
75
76    pub fn span_if_bad_stability(&self) -> Span {
77        let stability = format!("{}%", &self.stability * 100_f64);
78
79        if self.bad_stability() {
80            Span::styled(
81                format!("{stability} (the seeds are very unstable!!)",),
82                Style::default()
83                    .add_modifier(Modifier::BOLD)
84                    .add_modifier(Modifier::UNDERLINED)
85                    .underline_color(Color::White)
86                    .fg(Color::Red),
87            )
88        } else {
89            Span::styled(
90                stability.to_string(),
91                Style::default().add_modifier(Modifier::BOLD),
92            )
93        }
94    }
95}
96
97impl FromStr for AFLProperties {
98    type Err = Box<dyn std::error::Error>;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        let mut props = AFLProperties::default();
102
103        // Function to extract value using regex
104        fn extract_value<T: FromStr>(text: &str, pattern: &str) -> Option<T> {
105            Regex::new(pattern)
106                .ok()?
107                .captures(text)?
108                .get(1)?
109                .as_str()
110                .parse()
111                .ok()
112        }
113
114        if let Some(cap) = Regex::new(r"run time : (.+?)\s+β”‚").unwrap().captures(s) {
115            props.run_time = cap[1].to_string();
116        }
117
118        if let Some(cap) = Regex::new(r"last new find : (.+?)\s+β”‚")
119            .unwrap()
120            .captures(s)
121        {
122            props.last_new_find = cap[1].to_string();
123        }
124
125        if let Some(cap) = Regex::new(r"last saved crash : (.+?)\s+β”‚")
126            .unwrap()
127            .captures(s)
128        {
129            props.last_saved_crash = cap[1].to_string();
130        }
131
132        if let Some(cap) = Regex::new(r"stability : (.+?)\s+β”‚").unwrap().captures(s) {
133            let percentage_str = cap[1].to_string().replace("%", "");
134            let percentage: f64 = percentage_str.parse().unwrap();
135            props.stability = percentage / 100.0;
136        }
137
138        props.corpus_count = extract_value(s, r"corpus count : (\d+)").unwrap_or_default();
139        props.saved_crashes = extract_value(s, r"saved crashes : (\d+)").unwrap_or_default();
140        props.exec_speed = extract_value(s, r"exec speed : (\d+)").unwrap_or_default();
141
142        Ok(props)
143    }
144}
145#[derive(Clone, Default)]
146pub struct AFLDashboard {
147    pub log_fullpath: PathBuf,
148}
149
150impl Debug for AFLDashboard {
151    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
152        writeln!(f, "Path for the log: {:?}", self.log_fullpath)?;
153
154        let file = File::open(&self.log_fullpath).map_err(|_| fmt::Error)?;
155        let mut reader = BufReader::new(file);
156        reader.seek(SeekFrom::Start(0)).map_err(|_| fmt::Error)?;
157
158        if let Ok(lines) = reader.lines().collect::<io::Result<Vec<String>>>() {
159            let last_20_lines = lines
160                .iter()
161                .rev()
162                .take(20)
163                .cloned()
164                .collect::<Vec<String>>();
165            for line in last_20_lines.iter().rev() {
166                writeln!(f, "{line}")?;
167            }
168        }
169        Ok(())
170    }
171}
172
173impl FromPath for AFLDashboard {
174    type Output = AFLDashboard;
175
176    fn create_instance(log_fullpath: PathBuf) -> Self::Output {
177        AFLDashboard { log_fullpath }
178    }
179
180    fn get_filetype() -> PFiles {
181        PFiles::AFLLog
182    }
183}
184
185impl AFLDashboard {
186    /// Read and parse properties from the log file
187    pub fn read_properties(&self) -> ResultOf<AFLProperties> {
188        let content = self.show_log()?;
189
190        let delimiter = "AFL";
191        let dashboards: Vec<&str> = content.split(delimiter).collect();
192
193        if let Some(last_dashboard) = dashboards.last() {
194            let last_dashboard = format!("{}{}", delimiter, last_dashboard);
195
196            let cleaned = Regex::new(r"\x1b\[[^m]*m")?
197                .replace_all(&last_dashboard, "")
198                .to_string(); // remove ANSI for shell colors
199
200            return Self::parse_properties(&cleaned)
201        }
202        bail!("Couldn't parse the set of dashboards of AFL")
203    }
204
205    pub fn get_path(&self) -> PathBuf {
206        self.log_fullpath.to_path_buf()
207    }
208
209    pub fn show_log(&self) -> std::io::Result<String> {
210        fs::read_to_string(&self.log_fullpath)
211    }
212
213    // Function to parse properties using regex
214    fn parse_properties(content: &str) -> ResultOf<AFLProperties> {
215        match AFLProperties::from_str(content) {
216            Ok(e) => Ok(e),
217            Err(_) => bail!("Couldn't parse the AFL dashboard"),
218        }
219    }
220
221    // Check if the dashboard is ready based on specific content
222    pub fn is_ready(&self) -> bool {
223        fs::read_to_string(&self.log_fullpath)
224            .map(|content| content.contains("findings in depth"))
225            .unwrap_or(false)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    use crate::EmptyResult;
234    use std::io::Write;
235    use tempfile::NamedTempFile;
236
237    #[test]
238    fn test_is_ready() -> EmptyResult {
239        let afl_dashboard = "      AFL ++4.21c {mainaflfuzzer} (./target/afl/debug/phink) [explore]
240β”Œβ”€ process timing ────────────────────────────────────┬─ overall results ────┐
241β”‚        run time : 0 days, 0 hrs, 2 min, 49 sec      β”‚  cycles done : 312   β”‚
242β”‚   last new find : 0 days, 0 hrs, 2 min, 49 sec      β”‚ corpus count : 5     β”‚
243β”‚last saved crash : none seen yet                     β”‚saved crashes : 4     β”‚
244β”‚ last saved hang : none seen yet                     β”‚  saved hangs : 0     β”‚
245β”œβ”€ cycle progress ─────────────────────┬─ map coverage┴───────────────────────
246β”‚  now processing : 1.312 (20.0%)      β”‚    map density : 0.11% / 0.13%      β”‚
247β”‚  runs timed out : 0 (0.00%)          β”‚ count coverage : 54.00 bits/tuple   β”‚
248β”œβ”€ stage progress ─────────────────────┼─ findings in depth ──────────────────
249β”‚  now trying : havoc                  β”‚ favored items : 3 (60.00%)          β”‚
250β”‚ stage execs : 174/400 (43.50%)       β”‚  new edges on : 3 (60.00%)          β”‚
251β”‚ total execs : 1.51M                  β”‚ total crashes : 0 (0 saved)         β”‚
252β”‚  exec speed : 8726/sec               β”‚  total tmouts : 0 (0 saved)         β”‚
253β”œβ”€ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ────────
254β”‚   bit flips : 0/0, 0/0, 0/0                        β”‚    levels : 1         β”‚
255β”‚  byte flips : 0/0, 0/0, 0/0                        β”‚   pending : 0         β”‚
256β”‚ arithmetics : 0/0, 0/0, 0/0                        β”‚  pend fav : 0         β”‚
257β”‚  known ints : 0/0, 0/0, 0/0                        β”‚ own finds : 0         β”‚
258β”‚  dictionary : 0/0, 0/0, 0/0, 0/0                   β”‚  imported : 4         β”‚
259β”‚havoc/splice : 0/514k, 0/992k                       β”‚ stability : 100.00%   β”‚
260β”‚py/custom/rq : unused, unused, unused, unused       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
261β”‚    trim/eff : disabled, n/a                        β”‚          [cpu023: 51%]
262└─ strategy: explore ────────── state: started :-) β”€β”€β”˜";
263
264        let mut temp_file = NamedTempFile::new()?;
265        writeln!(temp_file, "{afl_dashboard}")?;
266        let path = temp_file.path();
267
268        let dashboard = AFLDashboard::from_fullpath(path.into())?;
269        assert!(dashboard.is_ready());
270        Ok(())
271    }
272
273    #[test]
274    fn test_spot_crashes() -> EmptyResult {
275        let afl_dashboard = "      AFL ++4.21c {mainaflfuzzer} (./target/afl/debug/phink) [explore]
276β”Œβ”€ process timing ────────────────────────────────────┬─ overall results ────┐
277β”‚        run time : 0 days, 0 hrs, 2 min, 1 sec      β”‚  cycles done : 312   β”‚
278β”‚   last new find : 0 days, 0 hrs, 2 min, 49 sec      β”‚ corpus count : 5     β”‚
279β”‚last saved crash : none seen yet                     β”‚saved crashes : 4     β”‚
280β”‚ last saved hang : none seen yet                     β”‚  saved hangs : 0     β”‚
281β”œβ”€ cycle progress ─────────────────────┬─ map coverage┴───────────────────────
282β”‚  now processing : 1.312 (20.0%)      β”‚    map density : 0.11% / 0.13%      β”‚
283β”‚  runs timed out : 0 (0.00%)          β”‚ count coverage : 54.00 bits/tuple   β”‚
284β”œβ”€ stage progress ─────────────────────┼─ findings in depth ──────────────────
285β”‚  now trying : havoc                  β”‚ favored items : 3 (60.00%)          β”‚
286β”‚ stage execs : 174/400 (43.50%)       β”‚  new edges on : 3 (60.00%)          β”‚
287β”‚ total execs : 1.51M                  β”‚ total crashes : 0 (0 saved)         β”‚
288β”‚  exec speed : 8726/sec               β”‚  total tmouts : 0 (0 saved)         β”‚
289β”œβ”€ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ────────
290β”‚   bit flips : 0/0, 0/0, 0/0                        β”‚    levels : 1         β”‚
291β”‚  byte flips : 0/0, 0/0, 0/0                        β”‚   pending : 0         β”‚
292β”‚ arithmetics : 0/0, 0/0, 0/0                        β”‚  pend fav : 0         β”‚
293β”‚  known ints : 0/0, 0/0, 0/0                        β”‚ own finds : 0         β”‚
294β”‚  dictionary : 0/0, 0/0, 0/0, 0/0                   β”‚  imported : 4         β”‚
295β”‚havoc/splice : 0/514k, 0/992k                       β”‚ stability : 97.42%   β”‚
296β”‚py/custom/rq : unused, unused, unused, unused       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
297β”‚    trim/eff : disabled, n/a                        β”‚          [cpu023: 51%]
298└─ strategy: explore ────────── state: started :-) β”€β”€β”˜
299
300";
301
302        let mut temp_file = NamedTempFile::new()?;
303        writeln!(temp_file, "{afl_dashboard}")?;
304        let path = temp_file.path();
305
306        let dashboard = AFLDashboard::from_fullpath(path.into())?;
307        let properties = dashboard.read_properties()?;
308
309        assert_eq!(properties.saved_crashes, 4);
310        assert_eq!(properties.run_time, "0 days, 0 hrs, 2 min, 1 sec");
311        assert_eq!(properties.exec_speed, 8726);
312        assert_eq!(properties.last_new_find, "0 days, 0 hrs, 2 min, 49 sec");
313        assert_eq!(properties.last_saved_crash, "none seen yet");
314        assert_eq!(properties.corpus_count, 5);
315        assert_eq!(properties.stability, 0.9742000000000001);
316
317        Ok(())
318    }
319
320    #[test]
321    fn test_no_crashes_or_hangs() -> EmptyResult {
322        let afl_dashboard = "      AFL ++4.21c {mainaflfuzzer} (./target/afl/debug/phink) [explore]
323β”Œβ”€ process timing ────────────────────────────────────┬─ overall results ────┐
324β”‚        run time : 0 days, 1 hrs, 30 min, 0 sec      β”‚  cycles done : 500   β”‚
325β”‚   last new find : 0 days, 1 hrs, 15 min, 30 sec     β”‚ corpus count : 10    β”‚
326β”‚last saved crash : none seen yet                     β”‚saved crashes : 0     β”‚
327β”‚ last saved hang : none seen yet                     β”‚  saved hangs : 0     β”‚
328β”œβ”€ cycle progress ─────────────────────┬─ map coverage┴───────────────────────
329β”‚  now processing : 2.500 (50.0%)      β”‚    map density : 0.15% / 0.18%      β”‚
330β”‚  runs timed out : 0 (0.00%)          β”‚ count coverage : 60.00 bits/tuple   β”‚
331β”œβ”€ stage progress ─────────────────────┼─ findings in depth ──────────────────
332β”‚  now trying : havoc                  β”‚ favored items : 5 (50.00%)          β”‚
333β”‚ stage execs : 250/500 (50.00%)       β”‚  new edges on : 5 (50.00%)          β”‚
334β”‚ total execs : 2.5M                   β”‚ total crashes : 0 (0 saved)         β”‚
335β”‚  exec speed : 10000/sec              β”‚  total tmouts : 0 (0 saved)         β”‚
336β”œβ”€ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ────────
337β”‚   bit flips : 0/0, 0/0, 0/0                        β”‚    levels : 2         β”‚
338β”‚  byte flips : 0/0, 0/0, 0/0                        β”‚   pending : 0         β”‚
339β”‚ arithmetics : 0/0, 0/0, 0/0                        β”‚  pend fav : 0         β”‚
340β”‚  known ints : 0/0, 0/0, 0/0                        β”‚ own finds : 5         β”‚
341β”‚  dictionary : 0/0, 0/0, 0/0, 0/0                   β”‚  imported : 5         β”‚
342β”‚havoc/splice : 0/1M, 0/1.5M                         β”‚ stability : 100.00%   β”‚
343β”‚py/custom/rq : unused, unused, unused, unused       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
344β”‚    trim/eff : disabled, n/a                        β”‚          [cpu047: 75%]
345└─ strategy: explore ────────── state: running β”€β”€β”€β”€β”€β”€β”˜";
346
347        let mut temp_file = NamedTempFile::new()?;
348        writeln!(temp_file, "{afl_dashboard}")?;
349        let path = temp_file.path();
350
351        let dashboard = AFLDashboard::from_fullpath(path.into())?;
352        let properties = dashboard.read_properties()?;
353
354        assert_eq!(properties.saved_crashes, 0);
355        assert_eq!(properties.run_time, "0 days, 1 hrs, 30 min, 0 sec");
356        assert_eq!(properties.exec_speed, 10000);
357        Ok(())
358    }
359
360    #[test]
361    fn test_with_crashes_and_hangs() -> EmptyResult {
362        let afl_dashboard = "      AFL ++4.21c {mainaflfuzzer} (./target/afl/debug/phink) [explore]
363β”Œβ”€ process timing ────────────────────────────────────┬─ overall results ────┐
364β”‚        run time : 1 days, 2 hrs, 45 min, 30 sec     β”‚  cycles done : 1000  β”‚
365β”‚   last new find : 0 days, 23 hrs, 59 min, 59 sec    β”‚ corpus count : 20    β”‚
366β”‚last saved crash : 0 days, 0 hrs, 15 min, 0 sec      β”‚saved crashes : 5     β”‚
367β”‚ last saved hang : 0 days, 1 hrs, 30 min, 0 sec      β”‚  saved hangs : 2     β”‚
368β”œβ”€ cycle progress ─────────────────────┬─ map coverage┴───────────────────────
369β”‚  now processing : 5.000 (100.0%)     β”‚    map density : 0.20% / 0.25%      β”‚
370β”‚  runs timed out : 2 (0.04%)          β”‚ count coverage : 75.00 bits/tuple   β”‚
371β”œβ”€ stage progress ─────────────────────┼─ findings in depth ──────────────────
372β”‚  now trying : havoc                  β”‚ favored items : 10 (50.00%)         β”‚
373β”‚ stage execs : 500/500 (100.00%)      β”‚  new edges on : 15 (75.00%)         β”‚
374β”‚ total execs : 5M                     β”‚ total crashes : 5 (5 saved)         β”‚
375β”‚  exec speed : 15000/sec              β”‚  total tmouts : 2 (2 saved)         β”‚
376β”œβ”€ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ────────
377β”‚   bit flips : 1/1M, 0/500k, 0/250k                 β”‚    levels : 3         β”‚
378β”‚  byte flips : 0/125k, 0/62k, 0/31k                 β”‚   pending : 0         β”‚
379β”‚ arithmetics : 0/15k, 0/7k, 0/3k                    β”‚  pend fav : 0         β”‚
380β”‚  known ints : 0/1k, 0/500, 0/250                   β”‚ own finds : 15        β”‚
381β”‚  dictionary : 0/100, 0/50, 0/25, 0/12              β”‚  imported : 5         β”‚
382β”‚havoc/splice : 4/2M, 0/2M                           β”‚ stability : 99.96%    β”‚
383β”‚py/custom/rq : unused, unused, unused, unused       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
384β”‚    trim/eff : 0.00%/5, disabled                    β”‚          [cpu095: 98%]
385└─ strategy: explore ────────── state: running β”€β”€β”€β”€β”€β”€β”˜";
386
387        let mut temp_file = NamedTempFile::new()?;
388        writeln!(temp_file, "{afl_dashboard}")?;
389        let path = temp_file.path();
390
391        let dashboard = AFLDashboard::from_fullpath(path.into())?;
392        let properties = dashboard.read_properties()?;
393
394        assert_eq!(properties.saved_crashes, 5);
395        assert_eq!(properties.run_time, "1 days, 2 hrs, 45 min, 30 sec");
396        assert_eq!(properties.last_saved_crash, "0 days, 0 hrs, 15 min, 0 sec");
397        assert_eq!(properties.exec_speed, 15000);
398        Ok(())
399    }
400
401    #[test]
402    fn test_with_real_fixture() -> EmptyResult {
403        let dashboard = AFLDashboard::from_fullpath(PathBuf::from("tests/fixtures/afl.log"))?;
404        let properties = dashboard.read_properties()?;
405        assert_eq!(properties.saved_crashes, 42);
406        assert_eq!(properties.run_time, "0 days, 0 hrs, 1 min, 21 sec");
407        assert_eq!(properties.last_saved_crash, "none seen yet");
408        assert_eq!(properties.exec_speed, 5555);
409        Ok(())
410    }
411}