From 2bcef074584a353d2fdb3fba29ccb46c4c0f1262 Mon Sep 17 00:00:00 2001 From: epi Date: Mon, 2 Sep 2024 10:20:02 -0400 Subject: [PATCH] implemented visible bar limiter --- Cargo.toml | 2 - Makefile.toml | 2 +- ferox-config.toml.example | 1 + shell_completions/_feroxbuster | 1 + shell_completions/_feroxbuster.ps1 | 1 + shell_completions/feroxbuster.bash | 6 +- shell_completions/feroxbuster.elv | 1 + src/banner/container.rs | 10 +++ src/config/container.rs | 8 +++ src/config/tests.rs | 9 +++ src/event_handlers/scans.rs | 2 +- src/parser.rs | 7 ++ src/progress.rs | 15 +++-- src/scan_manager/scan.rs | 96 ++++++++++++++++++++++++--- src/scan_manager/scan_container.rs | 102 ++++++++++++++++++++++++++--- src/scan_manager/tests.rs | 30 ++++++++- src/scanner/ferox_scanner.rs | 20 +++++- src/scanner/requester.rs | 6 +- src/scanner/tests.rs | 2 +- 19 files changed, 285 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9dc0350..ad7b83b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,6 @@ toml = "0.8" serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" uuid = { version = "1.10", features = ["v4"] } -# last known working version of indicatif; 0.17.5 has a bug that causes the -# scan menu to fail spectacularly indicatif = { version = "0.17.8" } console = "0.15" openssl = { version = "0.10", features = ["vendored"] } diff --git a/Makefile.toml b/Makefile.toml index f80e4309..49b8df0b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -14,7 +14,7 @@ rm ferox-*.state # dependency management [tasks.upgrade-deps] command = "cargo" -args = ["upgrade", "--exclude", "indicatif, self_update"] +args = ["upgrade", "--exclude", "self_update"] [tasks.update] command = "cargo" diff --git a/ferox-config.toml.example b/ferox-config.toml.example index 806461c5..bd8a0d9a 100644 --- a/ferox-config.toml.example +++ b/ferox-config.toml.example @@ -45,6 +45,7 @@ # dont_filter = true # extract_links = true # depth = 1 +# limit_bars = 3 # force_recursion = true # filter_size = [5174] # filter_regex = ["^ignore me$"] diff --git a/shell_completions/_feroxbuster b/shell_completions/_feroxbuster index d68b6e27..d78f388c 100644 --- a/shell_completions/_feroxbuster +++ b/shell_completions/_feroxbuster @@ -76,6 +76,7 @@ _feroxbuster() { '-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \ '--output=[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \ '--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]:FILE:_files' \ +'--limit-bars=[Number of directory scan bars to show at any given time (default\: no limit)]:NUM_BARS_TO_SHOW: ' \ '(-u --url)--stdin[Read url(s) from STDIN]' \ '(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http\://127.0.0.1\:8080 and set --insecure to true]' \ '(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http\://127.0.0.1\:8080 and set --insecure to true]' \ diff --git a/shell_completions/_feroxbuster.ps1 b/shell_completions/_feroxbuster.ps1 index 99a93f79..a738be3f 100644 --- a/shell_completions/_feroxbuster.ps1 +++ b/shell_completions/_feroxbuster.ps1 @@ -82,6 +82,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock { [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)') [CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)') [CompletionResult]::new('--debug-log', '--debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)') + [CompletionResult]::new('--limit-bars', '--limit-bars', [CompletionResultType]::ParameterName, 'Number of directory scan bars to show at any given time (default: no limit)') [CompletionResult]::new('--stdin', '--stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN') [CompletionResult]::new('--burp', '--burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true') [CompletionResult]::new('--burp-replay', '--burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true') diff --git a/shell_completions/feroxbuster.bash b/shell_completions/feroxbuster.bash index f8227b49..04c95065 100644 --- a/shell_completions/feroxbuster.bash +++ b/shell_completions/feroxbuster.bash @@ -19,7 +19,7 @@ _feroxbuster() { case "${cmd}" in feroxbuster) - opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --request-file --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --protocol --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --server-certs --client-cert --client-key --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --scan-dir-listings --verbosity --silent --quiet --json --output --debug-log --no-state --update --help --version" + opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --request-file --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --protocol --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --server-certs --client-cert --client-key --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --scan-dir-listings --verbosity --silent --quiet --json --output --debug-log --no-state --limit-bars --update --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -379,6 +379,10 @@ _feroxbuster() { fi return 0 ;; + --limit-bars) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/shell_completions/feroxbuster.elv b/shell_completions/feroxbuster.elv index 723fc80d..7c452c71 100644 --- a/shell_completions/feroxbuster.elv +++ b/shell_completions/feroxbuster.elv @@ -79,6 +79,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words| cand -o 'Output file to write results to (use w/ --json for JSON entries)' cand --output 'Output file to write results to (use w/ --json for JSON entries)' cand --debug-log 'Output file to write log entries (use w/ --json for JSON entries)' + cand --limit-bars 'Number of directory scan bars to show at any given time (default: no limit)' cand --stdin 'Read url(s) from STDIN' cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true' cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true' diff --git a/src/banner/container.rs b/src/banner/container.rs index 6eb52320..a427f3de 100644 --- a/src/banner/container.rs +++ b/src/banner/container.rs @@ -182,6 +182,9 @@ pub struct Banner { /// represents Configuration.scan_dir_listings scan_dir_listings: BannerEntry, + + /// represents Configuration.limit_bars + limit_bars: BannerEntry, } /// implementation of Banner @@ -358,6 +361,8 @@ impl Banner { let client_cert = BannerEntry::new("🏅", "Client Certificate", &config.client_cert); let client_key = BannerEntry::new("🔑", "Client Key", &config.client_key); let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string()); + let limit_bars = + BannerEntry::new("📊", "Limit Dir Scan Bars", &config.limit_bars.to_string()); let wordlist = BannerEntry::new("📖", "Wordlist", &config.wordlist); let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string()); let user_agent = BannerEntry::new("🦡", "User-Agent", &config.user_agent); @@ -474,6 +479,7 @@ impl Banner { config: cfg, scan_dir_listings, protocol, + limit_bars, version: VERSION.to_string(), update_status: UpdateStatus::Unknown, } @@ -618,6 +624,10 @@ by Ben "epi" Risher {} ver: {}"#, writeln!(&mut writer, "{}", self.protocol)?; } + if config.limit_bars > 0 { + writeln!(&mut writer, "{}", self.limit_bars)?; + } + if !config.config.is_empty() { writeln!(&mut writer, "{}", self.config)?; } diff --git a/src/config/container.rs b/src/config/container.rs index 3e720840..b1346cfb 100644 --- a/src/config/container.rs +++ b/src/config/container.rs @@ -344,6 +344,10 @@ pub struct Configuration { /// default request protocol #[serde(default = "request_protocol")] pub protocol: String, + + /// number of directory scan bars to show at any given time, 0 is no limit + #[serde(default)] + pub limit_bars: usize, } impl Default for Configuration { @@ -395,6 +399,7 @@ impl Default for Configuration { scan_limit: 0, parallel: 0, rate_limit: 0, + limit_bars: 0, add_slash: false, insecure: false, redirects: false, @@ -491,6 +496,7 @@ impl Configuration { /// - **depth**: `4` (maximum recursion depth) /// - **force_recursion**: `false` (still respects recursion depth) /// - **scan_limit**: `0` (no limit on concurrent scans imposed) + /// - **limit_bars**: `0` (no limit on number of directory scan bars shown) /// - **parallel**: `0` (no limit on parallel scans imposed) /// - **rate_limit**: `0` (no limit on requests per second imposed) /// - **time_limit**: `None` (no limit on length of scan imposed) @@ -644,6 +650,7 @@ impl Configuration { update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize); update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize); update_config_with_num_type_if_present!(&mut config.rate_limit, args, "rate_limit", usize); + update_config_with_num_type_if_present!(&mut config.limit_bars, args, "limit_bars", usize); update_config_if_present!(&mut config.wordlist, args, "wordlist", String); update_config_if_present!(&mut config.output, args, "output", String); update_config_if_present!(&mut config.debug_log, args, "debug_log", String); @@ -1132,6 +1139,7 @@ impl Configuration { update_if_not_default!(&mut conf.client_cert, new.client_cert, ""); update_if_not_default!(&mut conf.client_key, new.client_key, ""); update_if_not_default!(&mut conf.verbosity, new.verbosity, 0); + update_if_not_default!(&mut conf.limit_bars, new.limit_bars, 0); update_if_not_default!(&mut conf.silent, new.silent, false); update_if_not_default!(&mut conf.quiet, new.quiet, false); update_if_not_default!(&mut conf.auto_bail, new.auto_bail, false); diff --git a/src/config/tests.rs b/src/config/tests.rs index d95d3102..06affb33 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -49,6 +49,7 @@ fn setup_config_test() -> Configuration { json = true save_state = false depth = 1 + limit_bars = 3 protocol = "http" request_file = "/some/request/file" scan_dir_listings = true @@ -90,6 +91,7 @@ fn default_configuration() { assert_eq!(config.timeout, timeout()); assert_eq!(config.verbosity, 0); assert_eq!(config.scan_limit, 0); + assert_eq!(config.limit_bars, 0); assert!(!config.silent); assert!(!config.quiet); assert_eq!(config.output_level, OutputLevel::Default); @@ -266,6 +268,13 @@ fn config_reads_verbosity() { assert_eq!(config.verbosity, 1); } +#[test] +/// parse the test config and see that the value parsed is correct +fn config_reads_limit_bars() { + let config = setup_config_test(); + assert_eq!(config.limit_bars, 3); +} + #[test] /// parse the test config and see that the value parsed is correct fn config_reads_output() { diff --git a/src/event_handlers/scans.rs b/src/event_handlers/scans.rs index 5dc8ab92..5037e13d 100644 --- a/src/event_handlers/scans.rs +++ b/src/event_handlers/scans.rs @@ -120,7 +120,7 @@ impl ScanHandler { pub fn initialize(handles: Arc) -> (Joiner, ScanHandle) { log::trace!("enter: initialize"); - let data = Arc::new(FeroxScans::new(handles.config.output_level)); + let data = Arc::new(FeroxScans::new(handles.config.output_level, handles.config.limit_bars)); let (tx, rx): FeroxChannel = mpsc::unbounded_channel(); let max_depth = handles.config.depth; diff --git a/src/parser.rs b/src/parser.rs index 55669e60..31cf64b3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -660,6 +660,13 @@ pub fn initialize() -> Command { .num_args(0) .help_heading("Output settings") .help("Disable state output file (*.state)") + ).arg( + Arg::new("limit_bars") + .long("limit-bars") + .value_name("NUM_BARS_TO_SHOW") + .num_args(1) + .help_heading("Output settings") + .help("Number of directory scan bars to show at any given time (default: no limit)"), ); ///////////////////////////////////////////////////////////////////// diff --git a/src/progress.rs b/src/progress.rs index 42e235ad..372caf26 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -31,6 +31,15 @@ pub enum BarType { /// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html) /// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html) pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar { + let pb = ProgressBar::new(length).with_prefix(prefix.to_string()); + + update_style(&pb, bar_type); + + PROGRESS_BAR.add(pb) +} + +/// Update the style of a progress bar based on the `BarType` +pub fn update_style(bar: &ProgressBar, bar_type: BarType) { let mut style = ProgressStyle::default_bar().progress_chars("#>-").with_key( "smoothed_per_sec", |state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write| match ( @@ -66,11 +75,7 @@ pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar { BarType::Quiet => style.template("Scanning: {prefix}").unwrap(), }; - PROGRESS_BAR.add( - ProgressBar::new(length) - .with_style(style) - .with_prefix(prefix.to_string()), - ) + bar.set_style(style); } #[cfg(test)] diff --git a/src/scan_manager/scan.rs b/src/scan_manager/scan.rs index f2798169..398aea07 100644 --- a/src/scan_manager/scan.rs +++ b/src/scan_manager/scan.rs @@ -1,6 +1,7 @@ use super::*; use crate::{ config::OutputLevel, + progress::update_style, progress::{add_bar, BarType}, scanner::PolicyTrigger, }; @@ -16,10 +17,20 @@ use std::{ time::Instant, }; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use tokio::{sync, task::JoinHandle}; use uuid::Uuid; +#[derive(Debug, Default, Copy, Clone)] +pub enum Visibility { + /// whether a FeroxScan's progress bar is currently shown + #[default] + Visible, + + /// whether a FeroxScan's progress bar is currently hidden + Hidden, +} + /// Struct to hold scan-related state /// /// The purpose of this container is to open up the pathway to aborting currently running tasks and @@ -74,6 +85,12 @@ pub struct FeroxScan { /// tracker for the time at which this scan was started pub(super) start_time: Instant, + + /// whether the progress bar is currently visible or hidden + pub(super) visible: AtomicBool, + + /// stored value for Configuration.limit_bars + pub(super) bar_limit: usize, } /// Default implementation for FeroxScan @@ -86,6 +103,7 @@ impl Default for FeroxScan { id: new_id, task: sync::Mutex::new(None), // tokio mutex status: Mutex::new(ScanStatus::default()), + bar_limit: 0, num_requests: 0, requests_made_so_far: 0, scan_order: ScanOrder::Latest, @@ -98,12 +116,39 @@ impl Default for FeroxScan { status_429s: Default::default(), status_403s: Default::default(), start_time: Instant::now(), + visible: AtomicBool::new(true), } } } /// Implementation of FeroxScan impl FeroxScan { + /// return the visibility of the scan as a boolean + pub fn visible(&self) -> bool { + self.visible.load(Ordering::Relaxed) + } + + /// return the visibility of the scan as a boolean + pub fn running(&self) -> bool { + self.visible.load(Ordering::Relaxed) + } + + pub fn swap_visibility(&self, bar_type: BarType) { + // fetch_xor toggles the boolean to its opposite and returns the previous value + let visible = self.visible.fetch_xor(true, Ordering::Relaxed); + + if !visible { + // visibility was false before we xor'd the value + + let Ok(bar) = self.progress_bar.lock() else { + log::warn!("couldn't unlock progress bar for {}", self.url); + return; + }; + + update_style(bar.as_ref().unwrap(), bar_type); + } + } + /// Stop a currently running scan pub async fn abort(&self) -> Result<()> { log::trace!("enter: abort"); @@ -114,7 +159,8 @@ impl FeroxScan { log::trace!("aborting {:?}", self); task.abort(); self.set_status(ScanStatus::Cancelled)?; - self.stop_progress_bar(); + // todo: is this right? check it + self.stop_progress_bar(0); } } Err(e) => { @@ -151,15 +197,20 @@ impl FeroxScan { } /// Simple helper to call .finish on the scan's progress bar - pub(super) fn stop_progress_bar(&self) { + pub(super) fn stop_progress_bar(&self, active_bars: usize) { if let Ok(guard) = self.progress_bar.lock() { if guard.is_some() { let pb = (*guard).as_ref().unwrap(); + if self.bar_limit > 0 && self.bar_limit < active_bars + 1 { + pb.finish_and_clear(); + return; + } + if pb.position() > self.num_requests { - pb.finish() + pb.finish(); } else { - pb.abandon() + pb.abandon(); } } } @@ -177,7 +228,7 @@ impl FeroxScan { OutputLevel::Quiet => BarType::Quiet, OutputLevel::Silent | OutputLevel::SilentJSON => BarType::Hidden, }; - + // todo: add-bar check for limit/current let pb = add_bar(&self.url, self.num_requests, bar_type); pb.reset_elapsed(); @@ -196,7 +247,7 @@ impl FeroxScan { OutputLevel::Quiet => BarType::Quiet, OutputLevel::Silent | OutputLevel::SilentJSON => BarType::Hidden, }; - + // todo: add-bar check for limit/current let pb = add_bar(&self.url, self.num_requests, bar_type); pb.reset_elapsed(); @@ -206,6 +257,7 @@ impl FeroxScan { } /// Given a URL and ProgressBar, create a new FeroxScan, wrap it in an Arc and return it + #[allow(clippy::too_many_arguments)] pub fn new( url: &str, scan_type: ScanType, @@ -213,6 +265,8 @@ impl FeroxScan { num_requests: u64, output_level: OutputLevel, pb: Option, + visibility: bool, + bar_limit: usize, ) -> Arc { Arc::new(Self { url: url.to_string(), @@ -221,15 +275,17 @@ impl FeroxScan { scan_order, num_requests, output_level, + bar_limit, progress_bar: Mutex::new(pb), + visible: AtomicBool::new(visibility), ..Default::default() }) } /// Mark the scan as complete and stop the scan's progress bar - pub fn finish(&self) -> Result<()> { + pub fn finish(&self, n: usize) -> Result<()> { self.set_status(ScanStatus::Complete)?; - self.stop_progress_bar(); + self.stop_progress_bar(n); Ok(()) } @@ -262,6 +318,22 @@ impl FeroxScan { false } + /// small wrapper to inspect ScanStatus and see if it's Running + pub fn is_running(&self) -> bool { + if let Ok(guard) = self.status.lock() { + return matches!(*guard, ScanStatus::Running); + } + false + } + + /// small wrapper to inspect ScanStatus and see if it's NotStarted + pub fn is_not_started(&self) -> bool { + if let Ok(guard) = self.status.lock() { + return matches!(*guard, ScanStatus::NotStarted); + } + false + } + /// await a task's completion, similar to a thread's join; perform necessary bookkeeping pub async fn join(&self) { log::trace!("enter join({:?})", self); @@ -507,6 +579,8 @@ mod tests { 1000, OutputLevel::Default, None, + true, + 0, ); scan.add_error(); @@ -530,8 +604,10 @@ mod tests { normalized_url: String::from("/"), scan_type: ScanType::Directory, scan_order: ScanOrder::Initial, + bar_limit: 0, num_requests: 0, requests_made_so_far: 0, + visible: AtomicBool::new(true), status: Mutex::new(ScanStatus::Running), task: Default::default(), progress_bar: Mutex::new(None), @@ -551,7 +627,7 @@ mod tests { assert_eq!(req_sec, 100); - scan.finish().unwrap(); + scan.finish(0).unwrap(); assert_eq!(scan.requests_per_second(), 0); } } diff --git a/src/scan_manager/scan_container.rs b/src/scan_manager/scan_container.rs index 34107ccd..e3e651ac 100644 --- a/src/scan_manager/scan_container.rs +++ b/src/scan_manager/scan_container.rs @@ -20,6 +20,7 @@ use crate::{ use anyhow::Result; use console::style; use reqwest::StatusCode; +use scan::Visibility; use serde::{ser::SerializeSeq, Serialize, Serializer}; use std::{ collections::HashSet, @@ -61,6 +62,9 @@ pub struct FeroxScans { /// vector of extensions discovered and collected during scans pub(crate) collected_extensions: RwLock>, + + /// stored value for Configuration.limit_bars + bar_limit: usize, } /// Serialize implementation for FeroxScans @@ -93,9 +97,10 @@ impl Serialize for FeroxScans { /// Implementation of `FeroxScans` impl FeroxScans { /// given an OutputLevel, create a new FeroxScans object - pub fn new(output_level: OutputLevel) -> Self { + pub fn new(output_level: OutputLevel, bar_limit: usize) -> Self { Self { output_level, + bar_limit, ..Default::default() } } @@ -531,6 +536,8 @@ impl FeroxScans { for scan in scans.iter() { if scan.is_complete() { // these scans are complete, and just need to be shown to the user + // todo: this may change if i get the limited view working + // todo: add-bar check for limit/current let pb = add_bar( &scan.url, bar_length.try_into().unwrap_or_default(), @@ -595,6 +602,31 @@ impl FeroxScans { } } + /// determine the type of progress bar to display + /// takes both --limit-bars and output-level (--quiet|--silent|etc) + /// into account to arrive at a `BarType` + fn determine_bar_type(&self) -> BarType { + let visibility = if self.bar_limit == 0 { + // no limit from cli, just set the value to visible + // this protects us from a mutex unlock in number_of_bars + // in the normal case + Visibility::Visible + } else if self.bar_limit < self.number_of_bars() { + // active bars exceed limit; hidden + Visibility::Hidden + } else { + Visibility::Visible + }; + + match (self.output_level, visibility) { + (OutputLevel::Default, Visibility::Visible) => BarType::Default, + (OutputLevel::Quiet, Visibility::Visible) => BarType::Quiet, + (OutputLevel::Default, Visibility::Hidden) => BarType::Hidden, + (OutputLevel::Quiet, Visibility::Hidden) => BarType::Hidden, + (OutputLevel::Silent | OutputLevel::SilentJSON, _) => BarType::Hidden, + } + } + /// Given a url, create a new `FeroxScan` and add it to `FeroxScans` /// /// If `FeroxScans` did not already contain the scan, return true; otherwise return false @@ -612,14 +644,10 @@ impl FeroxScans { 0 }; + let bar_type = self.determine_bar_type(); + let bar = match scan_type { ScanType::Directory => { - let bar_type = match self.output_level { - OutputLevel::Default => BarType::Default, - OutputLevel::Quiet => BarType::Quiet, - OutputLevel::Silent | OutputLevel::SilentJSON => BarType::Hidden, - }; - let progress_bar = add_bar(url, bar_length, bar_type); progress_bar.reset_elapsed(); @@ -629,6 +657,8 @@ impl FeroxScans { ScanType::File => None, }; + let is_visible = !matches!(bar_type, BarType::Hidden); + let ferox_scan = FeroxScan::new( url, scan_type, @@ -636,6 +666,8 @@ impl FeroxScans { bar_length, self.output_level, bar, + is_visible, + self.bar_limit, ); // If the set did not contain the scan, true is returned. @@ -664,6 +696,58 @@ impl FeroxScans { self.add_scan(url, ScanType::File, scan_order) } + /// returns the number of active AND visible scans; supports --limit-bars functionality + pub fn number_of_bars(&self) -> usize { + let Ok(scans) = self.scans.read() else { + return 0; + }; + + let mut count = 0; + + for scan in &*scans { + if scan.is_active() && scan.visible() { + count += 1; + } + } + + count + } + + /// make one hidden bar visible; supports --limit-bars functionality + pub fn make_visible(&self) { + if let Ok(guard) = self.scans.read() { + // when swapping visibility, we'll prefer an actively running scan + // if none are found, we'll + let mut queued = None; + + for scan in &*guard { + if !matches!(scan.scan_type, ScanType::Directory) { + // visibility only makes sense for directory scans + continue; + } + + if scan.visible() { + continue; + } + + if scan.is_running() { + let bar_type = self.determine_bar_type(); + scan.swap_visibility(bar_type); + return; + } + + if queued.is_none() && scan.is_not_started() { + queued = Some(scan.clone()); + } + } + + if let Some(scan) = queued { + let bar_type = self.determine_bar_type(); + scan.swap_visibility(bar_type); + } + } + } + /// small helper to determine whether any scans are active or not pub fn has_active_scans(&self) -> bool { if let Ok(guard) = self.scans.read() { @@ -726,7 +810,7 @@ mod tests { #[test] /// unknown extension should be added to collected_extensions fn unknown_extension_is_added_to_collected_extensions() { - let scans = FeroxScans::new(OutputLevel::Default); + let scans = FeroxScans::new(OutputLevel::Default, 0); assert_eq!(0, scans.collected_extensions.read().unwrap().len()); @@ -739,7 +823,7 @@ mod tests { #[test] /// known extension should not be added to collected_extensions fn known_extension_is_added_to_collected_extensions() { - let scans = FeroxScans::new(OutputLevel::Default); + let scans = FeroxScans::new(OutputLevel::Default, 0); scans .collected_extensions .write() diff --git a/src/scan_manager/tests.rs b/src/scan_manager/tests.rs index 13cdae7f..b11dbfb8 100644 --- a/src/scan_manager/tests.rs +++ b/src/scan_manager/tests.rs @@ -15,6 +15,7 @@ use crate::{ use indicatif::ProgressBar; use predicates::prelude::*; use regex::Regex; +use std::sync::atomic::AtomicBool; use std::sync::{atomic::Ordering, Arc}; use std::thread::sleep; use std::time::Instant; @@ -75,6 +76,8 @@ fn add_url_to_list_of_scanned_urls_with_known_url() { pb.length().unwrap(), OutputLevel::Default, Some(pb), + true, + 0, ); assert!(urls.insert(scan)); @@ -97,6 +100,8 @@ fn stop_progress_bar_stops_bar() { pb.length().unwrap(), OutputLevel::Default, Some(pb), + true, + 0, ); assert!(!scan @@ -107,7 +112,7 @@ fn stop_progress_bar_stops_bar() { .unwrap() .is_finished()); - scan.stop_progress_bar(); + scan.stop_progress_bar(0); assert!(scan .progress_bar @@ -131,6 +136,8 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() { 0, OutputLevel::Default, None, + true, + 0, ); assert!(urls.insert(scan)); @@ -155,6 +162,8 @@ async fn call_display_scans() { pb.length().unwrap(), OutputLevel::Default, Some(pb), + true, + 0, ); let scan_two = FeroxScan::new( url_two, @@ -163,9 +172,11 @@ async fn call_display_scans() { pb_two.length().unwrap(), OutputLevel::Default, Some(pb_two), + true, + 0, ); - scan_two.finish().unwrap(); // one complete, one incomplete + scan_two.finish(0).unwrap(); // one complete, one incomplete scan_two .set_task(tokio::spawn(async move { sleep(Duration::from_millis(SLEEP_DURATION)); @@ -190,6 +201,8 @@ fn partial_eq_compares_the_id_field() { 0, OutputLevel::Default, None, + true, + 0, ); let scan_two = FeroxScan::new( url, @@ -198,6 +211,8 @@ fn partial_eq_compares_the_id_field() { 0, OutputLevel::Default, None, + true, + 0, ); assert!(!scan.eq(&scan_two)); @@ -280,6 +295,8 @@ fn ferox_scan_serialize() { 0, OutputLevel::Default, None, + true, + 0, ); let fs_json = format!( r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}"#, @@ -298,6 +315,8 @@ fn ferox_scans_serialize() { 0, OutputLevel::Default, None, + true, + 0, ); let ferox_scans = FeroxScans::default(); let ferox_scans_json = format!( @@ -360,6 +379,8 @@ fn feroxstates_feroxserialize_implementation() { 0, OutputLevel::Default, None, + true, + 0, ); let ferox_scans = FeroxScans::default(); let saved_id = ferox_scan.id.clone(); @@ -501,6 +522,7 @@ fn feroxstates_feroxserialize_implementation() { r#""method":"GET""#, r#""content_length":173"#, r#""line_count":10"#, + r#""limit_bars":0"#, r#""word_count":16"#, r#""headers""#, r#""server":"nginx/1.16.1"#, @@ -569,8 +591,10 @@ fn feroxscan_display() { normalized_url: String::from("http://localhost/"), scan_order: ScanOrder::Latest, scan_type: Default::default(), + bar_limit: 0, num_requests: 0, requests_made_so_far: 0, + visible: AtomicBool::new(true), start_time: Instant::now(), output_level: OutputLevel::Default, status_403s: Default::default(), @@ -615,10 +639,12 @@ async fn ferox_scan_abort() { normalized_url: String::from("http://localhost/"), scan_order: ScanOrder::Latest, scan_type: Default::default(), + bar_limit: 0, num_requests: 0, requests_made_so_far: 0, start_time: Instant::now(), output_level: OutputLevel::Default, + visible: AtomicBool::new(true), status_403s: Default::default(), status_429s: Default::default(), status: std::sync::Mutex::new(ScanStatus::Running), diff --git a/src/scanner/ferox_scanner.rs b/src/scanner/ferox_scanner.rs index 708fd708..980c86e5 100644 --- a/src/scanner/ferox_scanner.rs +++ b/src/scanner/ferox_scanner.rs @@ -11,7 +11,7 @@ use tokio::sync::Semaphore; use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter}; use crate::heuristics::WildcardResult; -use crate::Command::AddFilter; + use crate::Command::AddFilter; use crate::{ event_handlers::{ Command::{AddError, AddToF64Field, AddToUsizeField, SubtractFromUsizeField}, @@ -309,7 +309,14 @@ impl FeroxScanner { progress_bar.reset_eta(); progress_bar.finish_with_message(message); - ferox_scan.finish()?; + if self.handles.config.limit_bars > 0 { + let scans = self.handles.ferox_scans()?; + let num_bars = scans.number_of_bars(); + ferox_scan.finish(num_bars)?; + scans.make_visible(); + } else { + ferox_scan.finish(0)?; + } return Ok(()); // nothing left to do if we found a dir listing } @@ -392,7 +399,14 @@ impl FeroxScanner { _ = handle.await; } - ferox_scan.finish()?; + if self.handles.config.limit_bars > 0 { + let scans = self.handles.ferox_scans()?; + let num_bars = scans.number_of_bars(); + ferox_scan.finish(num_bars)?; + scans.make_visible(); + } else { + ferox_scan.finish(0)?; + } log::trace!("exit: scan_url"); diff --git a/src/scanner/requester.rs b/src/scanner/requester.rs index 0517ecab..b8c39e02 100644 --- a/src/scanner/requester.rs +++ b/src/scanner/requester.rs @@ -646,6 +646,8 @@ mod tests { 1000, OutputLevel::Default, None, + true, + 0, ); scan.set_status(ScanStatus::Running).unwrap(); @@ -1144,6 +1146,8 @@ mod tests { 1000, OutputLevel::Default, None, + true, + 0, ); scan.set_status(ScanStatus::Running).unwrap(); scan.add_429(); @@ -1177,7 +1181,7 @@ mod tests { 200 ); - scan.finish().unwrap(); + scan.finish(0).unwrap(); assert!(start.elapsed().as_millis() >= 2000); } } diff --git a/src/scanner/tests.rs b/src/scanner/tests.rs index d7e0323c..698f1641 100644 --- a/src/scanner/tests.rs +++ b/src/scanner/tests.rs @@ -15,7 +15,7 @@ use super::*; /// try to hit struct field coverage of FileOutHandler async fn get_scan_by_url_bails_on_unfound_url() { let sem = Semaphore::new(10); - let urls = FeroxScans::new(OutputLevel::Default); + let urls = FeroxScans::new(OutputLevel::Default, 0); let scanner = FeroxScanner::new( "http://localhost",