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 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 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(); 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 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 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}