1use crate::{
2 cli::{
3 ui::{
4 chart::ChartManager,
5 logger::LogDisplayer,
6 monitor::{
7 corpus::CorpusWatcher,
8 logs::{
9 AFLDashboard,
10 AFLProperties,
11 },
12 },
13 traits::{
14 FromPath,
15 Paint,
16 },
17 },
18 ziggy::ZiggyConfig,
19 },
20 instrumenter::instrumentation::Instrumenter,
21 EmptyResult,
22 ResultOf,
23};
24use anyhow::Context;
25use backend::CrosstermBackend;
26use contract_transcode::ContractMessageTranscoder;
27use ratatui::{
28 backend,
29 layout::{
30 Alignment,
31 Constraint,
32 Direction,
33 Layout,
34 Margin,
35 Rect,
36 },
37 style::{
38 Color,
39 Modifier,
40 Style,
41 Stylize,
42 },
43 symbols,
44 text::{
45 Line,
46 Span,
47 Text,
48 },
49 widgets::{
50 Block,
51 Borders,
52 Paragraph,
53 Sparkline,
54 SparklineBar,
55 },
56 Frame,
57};
58use std::{
59 borrow::Borrow,
60 collections::VecDeque,
61 fmt::Write,
62 io,
63 process::Child,
64 sync::{
65 atomic::{
66 AtomicBool,
67 Ordering,
68 },
69 Arc,
70 OnceLock,
71 },
72 thread::sleep,
73 time::Duration,
74};
75
76#[derive(Clone, Debug)]
77pub struct CustomUI {
78 ziggy_config: ZiggyConfig,
79 afl_dashboard: AFLDashboard,
80 corpus_watcher: CorpusWatcher,
81 fuzzing_speed: VecDeque<u64>,
82}
83
84pub static CTOR_VALUE: OnceLock<String> = OnceLock::new();
85
86impl CustomUI {
87 pub fn new(ziggy_config: &ZiggyConfig) -> ResultOf<CustomUI> {
88 CTOR_VALUE.get_or_init(|| {
89 if let Ok(maybe_metadata) = Instrumenter::new(ziggy_config.clone()).find() {
90 if let Ok(transcoder) = ContractMessageTranscoder::load(maybe_metadata.specs_path) {
91 if let Some(ctor) = &ziggy_config.clone().config().constructor_payload {
92 return if let Ok(encoded_bytes) = hex::decode(ctor) {
93 if let Ok(str) =
94 transcoder.decode_contract_constructor(&mut &encoded_bytes[..])
95 {
96 str.to_string()
97 } else {
98 format!("Couldn't decode {:?}", encoded_bytes)
99 }
100 } else {
101 "Double check your constructor in your `phink.toml`".to_string()
102 }
103 }
104 } else {
105 return "Couldn't load the JSON specs".parse().unwrap()
106 }
107 }
108 "-".into()
109 });
110
111 let output = ziggy_config.clone().fuzz_output();
112
113 Ok(Self {
114 ziggy_config: ziggy_config.clone(),
115 afl_dashboard: AFLDashboard::from_output(output.clone())
116 .context("Couldn't create AFL dashboard")?,
117 corpus_watcher: CorpusWatcher::from_output(output)
118 .context("Couldn't create the corpus watcher")?,
119 fuzzing_speed: VecDeque::new(),
120 })
121 }
122
123 fn ui(&mut self, f: &mut Frame) -> EmptyResult {
124 let chunks = Layout::default()
125 .direction(Direction::Vertical)
126 .margin(0)
127 .constraints(
128 [
129 Constraint::Length(7),
130 Constraint::Percentage(20),
131 Constraint::Percentage(50),
132 Constraint::Percentage(30),
133 ]
134 .as_ref(),
135 )
136 .split(f.area());
137
138 self.render_title(f, chunks[0]);
139 self.render_stats(f, chunks[1]);
140 self.render_chart_and_config(f, chunks[2]);
141 self.render_bottom(f, chunks[3])
142 .context("Couldn't render the bottom span")?;
143 Ok(())
144 }
145
146 fn render_chart_and_config(&mut self, f: &mut Frame, area: Rect) {
147 let chunks = Layout::default()
148 .direction(Direction::Horizontal)
149 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
150 .split(area);
151
152 self.render_chart(f, chunks[0]);
153 self.ziggy_config.config().render(f, chunks[1]);
154 }
155
156 fn render_octopus(&self, f: &mut Frame, area: Rect) {
157 let ascii_art = r#"
158,---.
159( @ @ )
160 ).-.(
161'/|||\`
162 '|`
163 "#;
164
165 let octopus = Paragraph::new(ascii_art)
166 .style(Style::default())
167 .alignment(Alignment::Center);
168 f.render_widget(octopus, area);
169 }
170 fn render_title(&self, f: &mut Frame, area: Rect) {
171 self.render_octopus(f, area);
172 let title = Paragraph::new("Phink Fuzzing Dashboard")
173 .style(
174 Style::default()
175 .fg(Color::White)
176 .add_modifier(Modifier::BOLD),
177 )
178 .alignment(Alignment::Center);
179 f.render_widget(title, area);
180 }
181
182 fn render_stats(&mut self, f: &mut Frame, area: Rect) {
183 let data = self.afl_dashboard.read_properties();
184
185 if let Ok(afl) = data {
186 self.update_fuzzing_speed(afl.exec_speed.into());
187 let chunks = Layout::default()
188 .direction(Direction::Horizontal)
189 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
190 .split(area);
191
192 let left_chunks = Layout::default()
193 .direction(Direction::Vertical)
194 .constraints([Constraint::Min(0)].as_ref())
195 .split(chunks[0]);
196
197 let right_chunk = Layout::default()
198 .direction(Direction::Vertical)
199 .constraints([Constraint::Min(0)].as_ref())
200 .split(chunks[1]);
201
202 self.stats_left(f, afl.borrow(), left_chunks[0]);
203 self.speed_right(f, right_chunk[0]);
204 }
205 }
206
207 fn stats_left(&self, frame: &mut Frame, data: &AFLProperties, area: Rect) {
208 let chunks = Layout::default()
209 .direction(Direction::Vertical)
210 .margin(1)
211 .constraints([Constraint::Percentage(100)].as_ref())
212 .split(area);
213
214 let paragraph = Paragraph::new(Vec::from([
215 Line::from(vec![
216 Span::raw("Running for: "),
217 Span::styled(
218 data.run_time.clone(),
219 Style::default().add_modifier(Modifier::BOLD),
220 ),
221 ]),
222 Line::from(vec![
223 Span::raw("Last new find: "),
224 Span::styled(
225 data.last_new_find.clone(),
226 Style::default().add_modifier(Modifier::BOLD),
227 ),
228 ]),
229 Line::from(vec![
230 Span::raw("Last saved crash: "),
231 Span::styled(
232 data.last_saved_crash.clone(),
233 Style::default().add_modifier(Modifier::BOLD),
234 ),
235 ]),
236 Line::from(vec![
237 Span::raw("Corpus count: "),
238 Span::styled(
239 data.corpus_count.to_string(),
240 Style::default().add_modifier(Modifier::BOLD),
241 ),
242 ]),
243 Line::from(vec![
244 Span::raw("Execution speed: "),
245 Span::styled(
246 format!("{} execs/sec", data.exec_speed),
247 Style::default().add_modifier(Modifier::BOLD),
248 ),
249 ]),
250 Line::from(vec![Span::raw("Stability: "), data.span_if_bad_stability()]),
251 Line::from(vec![Span::raw("Bug found: "), data.span_if_crash()]),
252 ]))
253 .block(
254 Block::default()
255 .borders(Borders::ALL)
256 .title("Statistics")
257 .bold()
258 .title_alignment(Alignment::Center),
259 );
260
261 frame.render_widget(paragraph, chunks[0]);
262 }
263
264 fn update_fuzzing_speed(&mut self, new_speed: u64) {
265 const MAX_POINTS: usize = 100; self.fuzzing_speed.push_back(new_speed);
268 if self.fuzzing_speed.len() > MAX_POINTS {
269 self.fuzzing_speed.pop_front();
270 }
271 }
272 fn speed_right(&mut self, frame: &mut Frame, area: Rect) {
273 let chunks = Layout::default()
274 .direction(Direction::Vertical)
275 .margin(1)
276 .constraints([Constraint::Percentage(100)].as_ref())
277 .split(area);
278
279 let speed_vec = &self.fuzzing_speed.make_contiguous();
280
281 let sparkline = Sparkline::default()
282 .block(
283 Block::new()
284 .borders(Borders::ALL)
285 .title("Execution speed evolution (execs/s)")
286 .bold()
287 .title_alignment(Alignment::Center),
288 )
289 .data(
290 speed_vec
291 .iter()
292 .map(|&value| SparklineBar::from(Some(value))),
293 )
294 .style(Style::default().fg(Color::White))
295 .bar_set(symbols::bar::NINE_LEVELS);
296
297 let stats_chunk = chunks[0].inner(Margin {
298 vertical: 1,
299 horizontal: 1,
300 });
301
302 frame.render_widget(sparkline, chunks[0]);
303
304 let stats = [
305 format!(
306 "Max: {:.2}",
307 self.fuzzing_speed
308 .iter()
309 .max_by(|a, b| a.partial_cmp(b).unwrap())
310 .unwrap_or(&0)
311 ),
312 format!(
313 "Avg: {:.2}",
314 self.fuzzing_speed.iter().sum::<u64>() / self.fuzzing_speed.len() as u64
315 ),
316 ];
317 for (i, stat) in stats.iter().enumerate() {
318 let stat_layout = Layout::default()
319 .direction(Direction::Horizontal)
320 .constraints([Constraint::Percentage(100)])
321 .split(Rect {
322 x: stats_chunk.x,
323 y: stats_chunk.y + i as u16,
324 width: stats_chunk.width,
325 height: 1,
326 });
327
328 let paragraph = Paragraph::new(stat.as_str()).style(
329 Style::default()
330 .fg(Color::White)
331 .add_modifier(Modifier::ITALIC),
332 );
333 frame.render_widget(paragraph, stat_layout[0]);
334 }
335 }
336
337 fn render_chart(&mut self, f: &mut Frame, area: Rect) {
338 let chunks = Layout::default()
339 .direction(Direction::Vertical)
340 .constraints([Constraint::Percentage(100)].as_ref())
341 .split(area);
342
343 let corpus_counter: &[(f64, f64)] = &self.corpus_watcher.as_tuple_slice();
344
345 let chart_manager = ChartManager::new(corpus_counter);
346 f.render_widget(chart_manager.create_chart(), chunks[0]);
347 }
348
349 fn render_bottom(&mut self, f: &mut Frame, area: Rect) -> EmptyResult {
350 let bottom_parts = Layout::default()
351 .direction(Direction::Horizontal)
352 .constraints([Constraint::Percentage(100)].as_ref())
353 .split(area);
354
355 let seed_info = self.display_fuzzed_seed();
356 f.render_widget(seed_info, bottom_parts[0]);
357
358 Ok(())
359 }
360
361 fn escape_non_printable(s: &str) -> String {
367 let mut result = String::with_capacity(s.len());
368 for byte in s.bytes() {
369 match byte {
370 0x20..=0x7E | b'\n' | b'\t' => result.push(byte as char),
371 _ => write!(result, "^{}", byte.wrapping_add(64) as char).unwrap(),
372 }
373 }
374 result
375 }
376 fn display_fuzzed_seed(&mut self) -> Paragraph {
377 let mut seed_text: Text = Default::default();
378 let seed_info_text: String =
379 match LogDisplayer::new(self.clone().ziggy_config.fuzz_output()).load() {
380 None => String::new(),
381 Some(e) => e.to_string(),
382 };
383
384 if !seed_info_text.is_empty() {
385 let escaped_text = Self::escape_non_printable(&seed_info_text);
386 for line in escaped_text.lines() {
387 seed_text.push_line(Line::styled(line.to_string(), Style::default()));
388 }
389 } else {
390 seed_text.push_span(Span::styled(
391 format!(
392 "Running the seeds, please wait until we actually start fuzzing...\n
393 If this screen get stuck for a while, execute `tail -f {}`.",
394 &self.afl_dashboard.get_path().to_str().unwrap()
395 ),
396 Style::default().fg(Color::Yellow),
397 ));
398 seed_text.push_span(Span::styled(
399 "Either there is a terrible bug (you'll see this in the AFL log), either we are still looking for a decodable seed.",
400 Style::default().fg(Color::Yellow),
401 ));
402 }
403
404 Paragraph::new(seed_text.clone()).block(
405 Block::default()
406 .borders(Borders::ALL)
407 .border_style(Style::default())
408 .title(Span::styled(
409 "Last Fuzzed Messages",
410 Style::default().add_modifier(Modifier::BOLD),
411 ))
412 .title_alignment(Alignment::Center),
413 )
414 }
415
416 pub fn initialize_tui(&mut self, mut child: Child) -> EmptyResult {
417 let backend = CrosstermBackend::new(io::stdout());
418 let mut terminal =
419 ratatui::Terminal::new(backend).context("Couldn't create the terminal backend")?;
420 terminal.clear().context("Couldn't clear the terminal")?;
421
422 let running = Arc::new(AtomicBool::new(true));
423 let r = running.clone();
424
425 ctrlc::set_handler(move || {
426 r.store(false, Ordering::SeqCst);
427 })?;
428
429 while running.load(Ordering::SeqCst) {
430 terminal.draw(|f| {
431 sleep(Duration::from_millis(500));
432 if let Err(err) = self.ui(f) {
433 eprintln!("{:?}", err);
434 }
435 })?;
436 }
437
438 let i = child.id();
439
440 terminal.clear()?;
441 child
442 .kill()
443 .context(format!("Couldn't kill the child n°{i}"))?;
444 println!("š It was nice fuzzing with you. Killing PID {i}. Bye bye! ",);
445
446 Ok(())
447 }
448}