diff --git a/AFLR_CFG_TEMPL.toml b/AFLR_CFG_TEMPL.toml index b6d4f71..a848c57 100644 --- a/AFLR_CFG_TEMPL.toml +++ b/AFLR_CFG_TEMPL.toml @@ -40,3 +40,7 @@ dry_run = false # Custom tmux session name session_name = "my_session" + +[misc] +# Enable TUI mode +tui = false diff --git a/Cargo.lock b/Cargo.lock index 63051ac..ffddbec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,17 +4,37 @@ version = 3 [[package]] name = "afl_runner" -version = "0.1.8" +version = "0.2.0" dependencies = [ "anyhow", "clap", + "crossterm", "rand", + "ratatui", "serde", "sysinfo", "toml", "upon", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.9" @@ -69,12 +89,39 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -110,7 +157,7 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -128,6 +175,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -159,6 +219,40 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "either" version = "1.9.0" @@ -197,6 +291,16 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -214,6 +318,27 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "libc" version = "0.2.153" @@ -226,12 +351,49 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -241,12 +403,62 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -301,6 +513,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +dependencies = [ + "bitflags 2.4.2", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "rayon" version = "1.8.1" @@ -321,33 +554,60 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "rustix" version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -363,12 +623,86 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.48" @@ -382,9 +716,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.10" +version = "0.30.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d7c217777061d5a2d652aea771fb9ba98b6dade657204b08c4b9604d11555b" +checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83" dependencies = [ "cfg-if", "core-foundation-sys", @@ -405,6 +739,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "toml" version = "0.8.12" @@ -445,6 +800,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.11" @@ -468,6 +829,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -655,3 +1022,23 @@ checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index eae5c61..d44521d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "afl_runner" authors = ["0x434b , #[arg( long, help = "Provide a TOML config for the configation", required = false )] + /// Path to a TOML config file pub config: Option, + /// Enable tui mode + #[arg(long, help = "Enable TUI mode", required = false)] + pub tui: bool, } #[derive(Deserialize, Default, Debug, Clone)] @@ -115,6 +120,7 @@ pub struct Config { pub target: TargetConfig, pub afl_cfg: AflConfig, pub tmux: TmuxConfig, + pub misc: MiscConfig, } #[derive(Deserialize, Default, Debug, Clone)] @@ -142,21 +148,50 @@ pub struct TmuxConfig { pub session_name: Option, } +#[derive(Deserialize, Default, Debug, Clone)] +pub struct MiscConfig { + pub tui: Option, +} + pub fn merge_args(cli_args: CliArgs, config_args: Config) -> CliArgs { + let dry_run = cli_args.dry_run || config_args.tmux.dry_run.unwrap_or(false); + let tui = if dry_run { + false + } else { + cli_args.tui || config_args.misc.tui.unwrap_or(false) + }; CliArgs { - target: cli_args - .target - .or_else(|| config_args.target.path.map(PathBuf::from)), - san_target: cli_args - .san_target - .or_else(|| config_args.target.san_path.map(PathBuf::from)), - cmpl_target: cli_args - .cmpl_target - .or_else(|| config_args.target.cmpl_path.map(PathBuf::from)), - cmpc_target: cli_args - .cmpc_target - .or_else(|| config_args.target.cmpc_path.map(PathBuf::from)), - target_args: cli_args.target_args.or(config_args.target.args), + target: cli_args.target.or_else(|| { + config_args + .target + .path + .filter(|p| !p.is_empty()) + .map(PathBuf::from) + }), + san_target: cli_args.san_target.or_else(|| { + config_args + .target + .san_path + .filter(|p| !p.is_empty()) + .map(PathBuf::from) + }), + cmpl_target: cli_args.cmpl_target.or_else(|| { + config_args + .target + .cmpl_path + .filter(|p| !p.is_empty()) + .map(PathBuf::from) + }), + cmpc_target: cli_args.cmpc_target.or_else(|| { + config_args + .target + .cmpc_path + .filter(|p| !p.is_empty()) + .map(PathBuf::from) + }), + target_args: cli_args + .target_args + .or_else(|| config_args.target.args.filter(|args| !args.is_empty())), runners: Some( cli_args .runners @@ -165,16 +200,45 @@ pub fn merge_args(cli_args: CliArgs, config_args: Config) -> CliArgs { ), input_dir: cli_args .input_dir - .or_else(|| config_args.afl_cfg.seed_dir.map(PathBuf::from)), + .or_else(|| { + config_args + .afl_cfg + .seed_dir + .filter(|d| !d.is_empty()) + .map(PathBuf::from) + }) + .or_else(|| { + // Provide a default path here + Some(PathBuf::from(AFL_CORPUS)) + }), output_dir: cli_args .output_dir - .or_else(|| config_args.afl_cfg.solution_dir.map(PathBuf::from)), - dictionary: cli_args - .dictionary - .or_else(|| config_args.afl_cfg.dictionary.map(PathBuf::from)), - afl_binary: cli_args.afl_binary.or(config_args.afl_cfg.afl_binary), - dry_run: cli_args.dry_run || config_args.tmux.dry_run.unwrap_or(false), - tmux_session_name: cli_args.tmux_session_name.or(config_args.tmux.session_name), + .or_else(|| { + config_args + .afl_cfg + .solution_dir + .filter(|d| !d.is_empty()) + .map(PathBuf::from) + }) + .or_else(|| { + // Provide a default path here + Some(PathBuf::from(AFL_OUTPUT)) + }), + dictionary: cli_args.dictionary.or_else(|| { + config_args + .afl_cfg + .dictionary + .filter(|d| !d.is_empty()) + .map(PathBuf::from) + }), + afl_binary: cli_args + .afl_binary + .or_else(|| config_args.afl_cfg.afl_binary.filter(|b| !b.is_empty())), + dry_run, + tmux_session_name: cli_args + .tmux_session_name + .or_else(|| config_args.tmux.session_name.filter(|s| !s.is_empty())), config: cli_args.config, + tui, } } diff --git a/src/data_collection.rs b/src/data_collection.rs new file mode 100644 index 0000000..d1a230e --- /dev/null +++ b/src/data_collection.rs @@ -0,0 +1,324 @@ +use crate::session::{CrashInfoDetails, SessionData}; +use std::fs; +use std::path::PathBuf; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub fn collect_session_data(output_dir: &PathBuf) -> SessionData { + let mut session_data = SessionData::new(); + + let mut fuzzers_alive = 0; + + for entry in fs::read_dir(output_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path.is_dir() { + let fuzzer_stats_path = path.join("fuzzer_stats"); + + if fuzzer_stats_path.exists() { + let content = fs::read_to_string(fuzzer_stats_path).unwrap(); + let lines: Vec<&str> = content.lines().collect(); + + for line in lines { + let parts: Vec<&str> = line.split(":").map(|s| s.trim()).collect(); + + if parts.len() == 2 { + let key = parts[0]; + let value = parts[1]; + + match key { + "start_time" => { + let start_time = UNIX_EPOCH + + Duration::from_secs(value.parse::().unwrap_or(0)); + let current_time = SystemTime::now(); + let duration = current_time.duration_since(start_time).unwrap(); + session_data.total_run_time = duration; + } + "execs_per_sec" => { + let exec_ps = value.parse::().unwrap_or(0.0); + if exec_ps > session_data.executions.ps_max { + session_data.executions.ps_max = exec_ps; + } else if exec_ps < session_data.executions.ps_min + || session_data.executions.ps_min == 0.0 + { + session_data.executions.ps_min = exec_ps; + } + session_data.executions.ps_cum += exec_ps; + } + "execs_done" => { + let execs_done = value.parse::().unwrap_or(0); + if execs_done > session_data.executions.max { + session_data.executions.max = execs_done; + } else if execs_done < session_data.executions.min + || session_data.executions.min == 0 + { + session_data.executions.min = execs_done; + } + session_data.executions.cum += execs_done; + } + "pending_favs" => { + let pending_favs = value.parse::().unwrap_or(0); + if pending_favs > session_data.pending.favorites_max { + session_data.pending.favorites_max = pending_favs; + } else if pending_favs < session_data.pending.favorites_min + || session_data.pending.favorites_min == 0 + { + session_data.pending.favorites_min = pending_favs; + } + session_data.pending.favorites_cum += pending_favs; + } + "pending_total" => { + let pending_total = value.parse::().unwrap_or(0); + if pending_total > session_data.pending.total_max { + session_data.pending.total_max = pending_total; + } else if pending_total < session_data.pending.total_min + || session_data.pending.total_min == 0 + { + session_data.pending.total_min = pending_total; + } + session_data.pending.total_cum += pending_total; + } + "stability" => { + let stability = + value.trim_end_matches('%').parse::().unwrap_or(0.0); + if stability > session_data.stability.max { + session_data.stability.max = stability; + } else if stability < session_data.stability.min + || session_data.stability.min == 0.0 + { + session_data.stability.min = stability; + } + } + "corpus_count" => { + let corpus_count = value.parse::().unwrap_or(0); + if corpus_count > session_data.corpus.count_max { + session_data.corpus.count_max = corpus_count; + } else if corpus_count < session_data.corpus.count_min + || session_data.corpus.count_min == 0 + { + session_data.corpus.count_min = corpus_count; + } + session_data.corpus.count_cum += corpus_count; + } + "bitmap_cvg" => { + let cvg = value.trim_end_matches('%').parse::().unwrap_or(0.0); + if cvg < session_data.coverage.bitmap_min + || session_data.coverage.bitmap_min == 0.0 + { + session_data.coverage.bitmap_min = cvg; + } else if cvg > session_data.coverage.bitmap_max { + session_data.coverage.bitmap_max = cvg; + } + } + "max_depth" => { + let levels = value.parse::().unwrap_or(0); + if levels > session_data.levels.max { + session_data.levels.max = levels; + } else if levels < session_data.levels.min + || session_data.levels.min == 0 + { + session_data.levels.min = levels; + } + } + "saved_crashes" => { + let saved_crashes = value.parse::().unwrap_or(0); + if saved_crashes > session_data.crashes.saved_max { + session_data.crashes.saved_max = saved_crashes; + } else if saved_crashes < session_data.crashes.saved_min + || session_data.crashes.saved_min == 0 + { + session_data.crashes.saved_min = saved_crashes; + } + session_data.crashes.saved_cum += saved_crashes; + } + "saved_hangs" => { + let saved_hangs = value.parse::().unwrap_or(0); + if saved_hangs > session_data.hangs.saved_max { + session_data.hangs.saved_max = saved_hangs; + } else if saved_hangs < session_data.hangs.saved_min + || session_data.hangs.saved_min == 0 + { + session_data.hangs.saved_min = saved_hangs; + } + session_data.hangs.saved_cum += saved_hangs; + } + "last_find" => { + let last_find = value.parse::().unwrap_or(0); + let last_find = UNIX_EPOCH + Duration::from_secs(last_find); + let current_time = SystemTime::now(); + let duration = current_time.duration_since(last_find).unwrap(); + if duration > session_data.time_without_finds { + session_data.time_without_finds = duration; + } + } + "afl_banner" => { + session_data.misc.afl_banner = value.to_string(); + } + "afl_version" => { + session_data.misc.afl_version = value.to_string(); + } + "cycles_done" => { + let cycles_done = value.parse::().unwrap_or(0); + if cycles_done > session_data.cycles.done_max { + session_data.cycles.done_max = cycles_done; + } else if cycles_done < session_data.cycles.done_min + || session_data.cycles.done_min == 0 + { + session_data.cycles.done_min = cycles_done; + } + } + "cycles_wo_finds" => { + let cycles_wo_finds = value.parse::().unwrap_or(0); + if cycles_wo_finds > session_data.cycles.wo_finds_max { + session_data.cycles.wo_finds_max = cycles_wo_finds; + } else if cycles_wo_finds < session_data.cycles.wo_finds_min + || session_data.cycles.wo_finds_min == 0 + { + session_data.cycles.wo_finds_min = cycles_wo_finds; + } + } + _ => {} + } + } + } + + fuzzers_alive += 1; + } + } + } + session_data.fuzzers_alive = fuzzers_alive; + + session_data.executions.ps_avg = if fuzzers_alive > 0 { + session_data.executions.ps_cum / fuzzers_alive as f64 + } else { + 0.0 + }; + session_data.executions.avg = if fuzzers_alive > 0 { + session_data.executions.cum / fuzzers_alive + } else { + 0 + }; + session_data.pending.favorites_avg = if fuzzers_alive > 0 { + session_data.pending.favorites_cum / fuzzers_alive + } else { + 0 + }; + session_data.pending.total_avg = if fuzzers_alive > 0 { + session_data.pending.total_cum / fuzzers_alive + } else { + 0 + }; + session_data.corpus.count_avg = if fuzzers_alive > 0 { + session_data.corpus.count_cum / fuzzers_alive + } else { + 0 + }; + session_data.crashes.saved_avg = if fuzzers_alive > 0 { + session_data.crashes.saved_cum / fuzzers_alive + } else { + 0 + }; + session_data.hangs.saved_avg = if fuzzers_alive > 0 { + session_data.hangs.saved_cum / fuzzers_alive + } else { + 0 + }; + session_data.coverage.bitmap_avg = + (session_data.coverage.bitmap_min + session_data.coverage.bitmap_max) / 2.0; + session_data.stability.avg = (session_data.stability.min + session_data.stability.max) / 2.0; + session_data.cycles.done_avg = + (session_data.cycles.done_min + session_data.cycles.done_max) / 2; + session_data.cycles.wo_finds_avg = + (session_data.cycles.wo_finds_min + session_data.cycles.wo_finds_max) / 2; + session_data.levels.avg = (session_data.levels.min + session_data.levels.max) / 2; + + let output_dir = output_dir.clone().into_os_string().into_string().unwrap(); + let (last_crashes, last_hangs) = collect_session_crashes_hangs(&output_dir, 10); + session_data.last_crashes = last_crashes; + session_data.last_hangs = last_hangs; + + session_data +} + +fn collect_session_crashes_hangs( + output_dir: &str, + num_latest: usize, +) -> (Vec, Vec) { + let mut crashes = Vec::new(); + let mut hangs = Vec::new(); + + for entry in fs::read_dir(output_dir).unwrap() { + let entry = entry.unwrap(); + let subdir = entry.path(); + + if subdir.is_dir() { + let fuzzer_name = subdir.file_name().unwrap().to_str().unwrap().to_string(); + + let crashes_dir = subdir.join("crashes"); + if crashes_dir.is_dir() { + process_files(&crashes_dir, &fuzzer_name, &mut crashes); + } + + let hangs_dir = subdir.join("hangs"); + if hangs_dir.is_dir() { + process_files(&hangs_dir, &fuzzer_name, &mut hangs); + } + } + } + + crashes.sort_by(|a, b| b.time.cmp(&a.time)); + hangs.sort_by(|a, b| b.time.cmp(&a.time)); + + ( + crashes.into_iter().take(num_latest).collect(), + hangs.into_iter().take(num_latest).collect(), + ) +} + +fn process_files(dir: &PathBuf, fuzzer_name: &str, file_infos: &mut Vec) { + for file_entry in fs::read_dir(dir).unwrap() { + if let Ok(file_entry) = file_entry { + let file = file_entry.path(); + if file.is_file() { + let filename = file.file_name().unwrap().to_str().unwrap(); + if let Some(file_info) = parse_filename(filename) { + let file_info = CrashInfoDetails { + fuzzer_name: fuzzer_name.to_string(), + file_path: file, + id: file_info.0, + sig: file_info.1, + src: file_info.2, + time: file_info.3, + execs: file_info.4, + op: file_info.5, + rep: file_info.6, + }; + file_infos.push(file_info); + } + } + } + } +} + +fn parse_filename( + filename: &str, +) -> Option<(String, Option, String, u64, u64, String, u64)> { + let parts: Vec<&str> = filename.split(',').collect(); + if parts.len() == 6 || parts.len() == 7 { + let id = parts[0].split(':').nth(1)?.to_string(); + let sig = if parts.len() == 7 { + Some(parts[1].split(':').nth(1)?.to_string()) + } else { + None + }; + let src_index = if sig.is_some() { 2 } else { 1 }; + let src = parts[src_index].split(':').nth(1)?.to_string(); + let time = parts[src_index + 1].split(':').nth(1)?.parse().ok()?; + let execs = parts[src_index + 2].split(':').nth(1)?.parse().ok()?; + let op = parts[src_index + 3].split(':').nth(1)?.to_string(); + let rep = parts[src_index + 4].split(':').nth(1)?.parse().ok()?; + Some((id, sig, src, time, execs, op, rep)) + } else { + None + } +} diff --git a/src/main.rs b/src/main.rs index d9392f9..e69f7d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ +use anyhow::Result; use clap::Parser; use cli::{merge_args, CliArgs, Config}; use std::env; +use std::fs; use std::hash::{DefaultHasher, Hasher}; use std::path::PathBuf; +use std::sync::mpsc; +use std::thread; mod afl_cmd_gen; mod afl_env; @@ -12,27 +16,16 @@ mod tmux; use afl_cmd_gen::AFLCmdGenerator; use harness::Harness; use tmux::Session; +mod data_collection; +mod session; +mod tui; use crate::cli::AFL_CORPUS; fn main() { let cli_args = CliArgs::parse(); - let config_args: Config = cli_args.config.as_deref().map_or_else( - || { - let cwd = env::current_dir().unwrap(); - let default_config_path = cwd.join("aflr_cfg.toml"); - if default_config_path.exists() { - let config_content = std::fs::read_to_string(&default_config_path).unwrap(); - toml::from_str(&config_content).unwrap() - } else { - Config::default() - } - }, - |config_path| { - let config_content = std::fs::read_to_string(config_path).unwrap(); - toml::from_str(&config_content).unwrap() - }, - ); + let config_args: Config = load_config(&cli_args); + let raw_afl_flags = config_args.afl_cfg.afl_flags.clone(); let args = merge_args(cli_args, config_args); @@ -43,12 +36,36 @@ fn main() { if args.dry_run { print_generated_commands(&cmds); + return; + } + + let tmux_name = generate_tmux_name(&args, &target_args); + if args.tui { + run_tmux_session_with_tui(&tmux_name, &cmds, &args); } else { - let tmux_name = generate_tmux_name(&args, &target_args); run_tmux_session(&tmux_name, &cmds); } } +fn load_config(cli_args: &CliArgs) -> Config { + match cli_args.config.as_deref() { + Some(config_path) => { + let config_content = fs::read_to_string(config_path).unwrap(); + toml::from_str(&config_content).unwrap() + } + None => { + let cwd = env::current_dir().unwrap(); + let default_config_path = cwd.join("aflr_cfg.toml"); + if default_config_path.exists() { + let config_content = fs::read_to_string(&default_config_path).unwrap(); + toml::from_str(&config_content).unwrap() + } else { + Config::default() + } + } + } +} + fn create_harness(args: &CliArgs) -> Harness { Harness::new( args.target.clone().unwrap(), @@ -117,10 +134,42 @@ fn generate_tmux_name(args: &CliArgs, target_args: &str) -> String { fn run_tmux_session(tmux_name: &str, cmds: &[String]) { let tmux = Session::new(tmux_name, cmds); if let Err(e) = tmux.run() { - eprintln!("Error running tmux session: {e}"); let _ = tmux.kill_session(); + eprintln!("Error running tmux session: {}", e); } else { tmux.attach().unwrap(); } } +fn run_tmux_session_with_tui(tmux_name: &str, cmds: &[String], args: &CliArgs) { + if let Err(e) = run_tmux_session_detached(tmux_name, cmds) { + eprintln!("Error running TUI: {}", e); + return; + } + let (session_data_tx, session_data_rx) = mpsc::channel(); + let output_dir = args.output_dir.clone().unwrap(); + + thread::spawn(move || loop { + let session_data = data_collection::collect_session_data(&output_dir); + if let Err(e) = session_data_tx.send(session_data) { + eprintln!("Error sending session data: {}", e); + break; + } + thread::sleep(std::time::Duration::from_secs(1)); + }); + + if let Err(e) = tui::run_tui(session_data_rx) { + eprintln!("Error running TUI: {}", e); + } +} + +fn run_tmux_session_detached(tmux_name: &str, cmds: &[String]) -> Result<()> { + let tmux = Session::new(tmux_name, cmds); + if let Err(e) = tmux.run() { + let _ = tmux.kill_session(); + return Err(e); + } else { + println!("Session {tmux_name} started in detached mode"); + } + Ok(()) +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..5cf7b3d --- /dev/null +++ b/src/session.rs @@ -0,0 +1,232 @@ +use std::path::PathBuf; +use std::time::Duration; + +pub struct CrashInfoDetails { + pub fuzzer_name: String, + pub file_path: PathBuf, + pub id: String, + pub sig: Option, + pub src: String, + pub time: u64, + pub execs: u64, + pub op: String, + pub rep: u64, +} + +pub struct SessionData { + pub fuzzers_alive: usize, + pub total_run_time: Duration, + pub executions: ExecutionsInfo, + pub pending: PendingInfo, + pub corpus: CorpusInfo, + pub coverage: CoverageInfo, + pub cycles: Cycles, + pub stability: StabilityInfo, + pub crashes: CrashInfo, + pub hangs: CrashInfo, + pub levels: Levels, + pub time_without_finds: Duration, + pub last_crashes: Vec, + pub last_hangs: Vec, + pub misc: Misc, +} + +impl Default for SessionData { + fn default() -> Self { + Self { + fuzzers_alive: 0, + total_run_time: Duration::from_secs(0), + executions: ExecutionsInfo::default(), + pending: PendingInfo::default(), + corpus: CorpusInfo::default(), + coverage: CoverageInfo::default(), + cycles: Cycles::default(), + stability: StabilityInfo::default(), + crashes: CrashInfo::default(), + hangs: CrashInfo::default(), + levels: Levels::default(), + time_without_finds: Duration::from_secs(0), + last_crashes: Vec::with_capacity(10), + last_hangs: Vec::with_capacity(10), + misc: Misc::default(), + } + } +} + +impl SessionData { + pub fn new() -> Self { + Self::default() + } +} + +pub struct Levels { + pub avg: usize, + pub min: usize, + pub max: usize, +} + +impl Default for Levels { + fn default() -> Self { + Self { + avg: 0, + min: 0, + max: 0, + } + } +} + +pub struct CrashInfo { + pub saved_cum: usize, + pub saved_avg: usize, + pub saved_min: usize, + pub saved_max: usize, +} + +impl Default for CrashInfo { + fn default() -> Self { + Self { + saved_cum: 0, + saved_avg: 0, + saved_min: 0, + saved_max: 0, + } + } +} + +pub struct StabilityInfo { + pub avg: f64, + pub min: f64, + pub max: f64, +} + +impl Default for StabilityInfo { + fn default() -> Self { + Self { + avg: 0.0, + min: 0.0, + max: 0.0, + } + } +} + +pub struct Cycles { + pub done_avg: usize, + pub done_min: usize, + pub done_max: usize, + pub wo_finds_avg: usize, + pub wo_finds_min: usize, + pub wo_finds_max: usize, +} + +impl Default for Cycles { + fn default() -> Self { + Self { + done_avg: 0, + done_min: 0, + done_max: 0, + wo_finds_avg: 0, + wo_finds_min: 0, + wo_finds_max: 0, + } + } +} + +pub struct ExecutionsInfo { + pub avg: usize, + pub min: usize, + pub max: usize, + pub cum: usize, + pub ps_avg: f64, + pub ps_min: f64, + pub ps_max: f64, + pub ps_cum: f64, +} + +impl Default for ExecutionsInfo { + fn default() -> Self { + Self { + avg: 0, + min: 0, + max: 0, + cum: 0, + ps_avg: 0.0, + ps_min: 0.0, + ps_max: 0.0, + ps_cum: 0.0, + } + } +} + +pub struct CoverageInfo { + pub bitmap_avg: f64, + pub bitmap_min: f64, + pub bitmap_max: f64, +} + +impl Default for CoverageInfo { + fn default() -> Self { + Self { + bitmap_avg: 0.0, + bitmap_min: 0.0, + bitmap_max: 0.0, + } + } +} + +pub struct PendingInfo { + pub favorites_avg: usize, + pub favorites_cum: usize, + pub favorites_max: usize, + pub favorites_min: usize, + pub total_avg: usize, + pub total_cum: usize, + pub total_min: usize, + pub total_max: usize, +} + +impl Default for PendingInfo { + fn default() -> Self { + Self { + favorites_avg: 0, + favorites_cum: 0, + favorites_max: 0, + favorites_min: 0, + total_avg: 0, + total_cum: 0, + total_min: 0, + total_max: 0, + } + } +} + +pub struct CorpusInfo { + pub count_avg: usize, + pub count_cum: usize, + pub count_min: usize, + pub count_max: usize, +} + +impl Default for CorpusInfo { + fn default() -> Self { + Self { + count_avg: 0, + count_cum: 0, + count_min: 0, + count_max: 0, + } + } +} + +pub struct Misc { + pub afl_version: String, + pub afl_banner: String, +} + +impl Default for Misc { + fn default() -> Self { + Self { + afl_version: String::new(), + afl_banner: String::new(), + } + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..5b49c96 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,304 @@ +use std::io; +use std::sync::mpsc; +use std::time::Duration; + +use crate::session::{CrashInfoDetails, SessionData}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + prelude::*, + style::{Color, Style}, + widgets::{Block, Borders, Paragraph}, + Terminal, +}; + +pub fn run_tui(session_data_rx: mpsc::Receiver) -> io::Result<()> { + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + terminal.clear()?; + enable_raw_mode()?; + crossterm::execute!(terminal.backend_mut(), EnterAlternateScreen)?; + + loop { + if let Ok(session_data) = session_data_rx.recv_timeout(Duration::from_millis(500)) { + terminal.draw(|f| { + let title = create_title(&session_data); + let main_chunks = create_main_layout(f.size()); + f.render_widget(title, main_chunks[0]); + + let inner_chunks = create_inner_layout(main_chunks[1]); + render_process_timings_and_overall_results(f, &session_data, inner_chunks[0]); + render_stage_progress_and_nerd_stats(f, &session_data, inner_chunks[1]); + render_crash_solutions(f, &session_data, inner_chunks[2]); + render_hang_solutions(f, &session_data, inner_chunks[3]); + })?; + } + + if crossterm::event::poll(Duration::from_millis(200))? { + if let crossterm::event::Event::Key(_) = crossterm::event::read()? { + break; + } + } + } + + disable_raw_mode()?; + crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.clear()?; + terminal.show_cursor()?; + + Ok(()) +} + +fn create_title(session_data: &SessionData) -> Paragraph { + Paragraph::new(format!( + "AFL {} - {} - Fuzzing campaign runner by @0xricksanchez", + session_data.misc.afl_version, session_data.misc.afl_banner + )) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) +} + +fn create_main_layout(size: Rect) -> Vec { + Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(size) + .to_vec() +} + +fn create_inner_layout(area: Rect) -> Vec { + Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(30), + Constraint::Percentage(30), + ] + .as_ref(), + ) + .split(area) + .to_vec() +} + +fn render_process_timings_and_overall_results( + f: &mut Frame, + session_data: &SessionData, + area: Rect, +) { + let hor_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .split(area); + + let p_proc_timings = create_process_timings_paragraph(session_data); + let p_overall_res = create_overall_results_paragraph(session_data); + + f.render_widget(p_proc_timings, hor_layout[0]); + f.render_widget(p_overall_res, hor_layout[1]); +} + +fn render_stage_progress_and_nerd_stats(f: &mut Frame, session_data: &SessionData, area: Rect) { + let hor_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .split(area); + + let p_stage_prog = create_stage_progress_paragraph(session_data); + let p_nerd_stats = create_nerd_stats_paragraph(session_data); + + f.render_widget(p_stage_prog, hor_layout[0]); + f.render_widget(p_nerd_stats, hor_layout[1]); +} + +fn render_crash_solutions(f: &mut Frame, session_data: &SessionData, area: Rect) { + let p_crash_solutions = Paragraph::new(format_solutions(&session_data.last_crashes)) + .block( + Block::default() + .title("10 Latest Crashes") + .borders(Borders::ALL) + .border_style(Style::default()), + ) + .style(Style::default()); + + f.render_widget(p_crash_solutions, area); +} + +fn render_hang_solutions(f: &mut Frame, session_data: &SessionData, area: Rect) { + let p_hang_solutions = Paragraph::new(format_solutions(&session_data.last_hangs)) + .block( + Block::default() + .title("10 Latest Hangs") + .borders(Borders::ALL) + .border_style(Style::default()), + ) + .style(Style::default()); + + f.render_widget(p_hang_solutions, area); +} + +fn create_process_timings_paragraph(session_data: &SessionData) -> Paragraph { + let last_seen_crash = + format_last_event(&session_data.last_crashes, &session_data.total_run_time); + let last_seen_hang = format_last_event(&session_data.last_hangs, &session_data.total_run_time); + + let content = format!( + "Fuzzers alive: {} +Total run time: {} +Time without finds: {} +Last saved crash: {} +Last saved hang: {}", + session_data.fuzzers_alive, + format_duration(session_data.total_run_time), + format_duration(session_data.time_without_finds), + last_seen_crash, + last_seen_hang + ); + + Paragraph::new(content) + .block( + Block::default() + .title("Process timing") + .borders(Borders::ALL) + .add_modifier(Modifier::BOLD), + ) + .style(Style::default()) +} + +fn create_overall_results_paragraph(session_data: &SessionData) -> Paragraph { + let content = format!( + "Cycles done: {} ({}/{}) +Crashes saved: {} ({}->{}<-{}) +Hangs saved: {} ({}->{}<-{}) +Corpus count: {} ({}->{}<-{})", + session_data.cycles.done_avg, + session_data.cycles.done_min, + session_data.cycles.done_max, + session_data.crashes.saved_cum, + session_data.crashes.saved_min, + session_data.crashes.saved_avg, + session_data.crashes.saved_max, + session_data.hangs.saved_cum, + session_data.hangs.saved_min, + session_data.hangs.saved_avg, + session_data.hangs.saved_max, + session_data.corpus.count_cum, + session_data.corpus.count_min, + session_data.corpus.count_avg, + session_data.corpus.count_max + ); + + Paragraph::new(content) + .block( + Block::default() + .title("Overall results") + .borders(Borders::ALL) + .add_modifier(Modifier::BOLD), + ) + .style(Style::default()) +} + +fn create_stage_progress_paragraph(session_data: &SessionData) -> Paragraph { + let content = format!( + "Execs: {} ({}->{}<-{}) +Execs/s: {:.2} ({:.2}->{:.2}<-{:.2}), +Coverage: {:.2}% ({:.2}%/{:.2}%)", + session_data.executions.cum, + session_data.executions.min, + session_data.executions.avg, + session_data.executions.max, + session_data.executions.ps_cum, + session_data.executions.ps_min, + session_data.executions.ps_avg, + session_data.executions.ps_max, + session_data.coverage.bitmap_avg, + session_data.coverage.bitmap_min, + session_data.coverage.bitmap_max, + ); + + Paragraph::new(content) + .block( + Block::default() + .title("Stage Progress") + .borders(Borders::ALL) + .add_modifier(Modifier::BOLD), + ) + .style(Style::default()) +} + +fn create_nerd_stats_paragraph(session_data: &SessionData) -> Paragraph { + let content = format!( + "Levels: {} ({}/{}) +Pending favorites: {} ({}->{}<-{}) +Pending total: {} ({}->{}<-{}), +Cycles without finds: {} ({}/{})", + session_data.levels.avg, + session_data.levels.min, + session_data.levels.max, + session_data.pending.favorites_cum, + session_data.pending.favorites_min, + session_data.pending.favorites_avg, + session_data.pending.favorites_max, + session_data.pending.total_cum, + session_data.pending.total_min, + session_data.pending.total_avg, + session_data.pending.total_max, + session_data.cycles.wo_finds_avg, + session_data.cycles.wo_finds_min, + session_data.cycles.wo_finds_max + ); + + Paragraph::new(content) + .block(Block::default().title("Nerd Stats").borders(Borders::ALL)) + .style(Style::default()) +} + +fn format_last_event(events: &[CrashInfoDetails], total_run_time: &Duration) -> String { + if !events.is_empty() { + let event_time = *total_run_time - Duration::from_millis(events[0].time); + format_duration(event_time) + } else { + "N/A".to_string() + } +} + +fn format_solutions(solutions: &[CrashInfoDetails]) -> String { + solutions + .iter() + .map(|s| { + format!( + "{} | SIG: {} | TIME: {} | EXEC: {} | SRC: {} | OP: {} | REP: {}", + s.fuzzer_name, + s.sig.clone().unwrap_or_else(|| "-".to_string()), + s.time, + s.execs, + s.src, + s.op, + s.rep + ) + }) + .collect::>() + .join("\n") +} + +fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + let secs = secs % 60; + + format!("{} days, {:02}:{:02}:{:02}", days, hours, mins, secs) +}