diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 47b50cf..78d4784 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT - [ ] Your PR description references the associated issue (i.e. fixes #123456) - [ ] Code is in its own branch - [ ] Branch name is related to the PR contents -- [ ] PR targets master +- [ ] PR targets main ## Static analysis checks - [ ] All rust files are formatted using `cargo fmt` diff --git a/Cargo.lock b/Cargo.lock index eefc507..ee19827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "feroxbuster" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index b620acd..696023c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "feroxbuster" -version = "2.1.0" +version = "2.2.0" authors = ["Ben 'epi' Risher "] license = "MIT" edition = "2018" @@ -22,7 +22,7 @@ lazy_static = "1.4" [dependencies] futures = { version = "0.3"} -tokio = { version = "1.0", features = ["full"] } +tokio = { version = "1.2.0", features = ["full"] } tokio-util = {version = "0.6.3", features = ["codec"]} log = "0.4" env_logger = "0.8.3" @@ -31,7 +31,7 @@ clap = "2.33" lazy_static = "1.4" toml = "0.5" serde = { version = "1.0", features = ["derive", "rc"] } -serde_json = "1.0" +serde_json = "1.0.62" uuid = { version = "0.8", features = ["v4"] } indicatif = "0.15" console = "0.14" diff --git a/README.md b/README.md index 740ab68..d808b7c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Enumeration. - [Limit Number of Requests per Second (Rate Limiting) (new in `v2.0.0`)](#limit-number-of-requests-per-second-rate-limiting-new-in-v200) - [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200) - [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210) + - [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220) - [Comparison w/ Similar Tools](#-comparison-w-similar-tools) - [Common Problems/Issues (FAQ)](#-common-problemsissues-faq) - [No file descriptors available](#no-file-descriptors-available) @@ -370,6 +371,7 @@ A pre-made configuration file with examples of all available settings can be fou # status_codes = [200, 500] # filter_status = [301] # threads = 1 +# parallel = 2 # timeout = 5 # auto_tune = true # auto_bail = true @@ -463,6 +465,9 @@ OPTIONS: -W, --filter-words ... Filter out messages of a particular word count (ex: -W 312 -W 91,82) -H, --headers
... Specify HTTP headers (ex: -H Header:val 'stuff: things') -o, --output Output file to write results to (use w/ --json for JSON entries) + --parallel + Run parallel feroxbuster instances (one child process per url passed via stdin) + -p, --proxy Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port) @@ -898,6 +903,29 @@ The AutoBail policy aborts individual directory scans when one of the criteria a ![auto-bail](img/auto-bail-demo.gif) +### Run Scans in Parallel (new in `v2.2.0`) + +Version 2.2.0 introduces the `--parallel` option. If you're one of those people who use `feroxbuster` to scan 100s of hosts at a time, this is the option for you! `--parallel` spawns a child process per target passed in over stdin (recursive directories are still async within each child). + +The number of parallel scans is limited to whatever you pass to `--parallel`. When one child finishes its scan, the next child will be spawned. + +Unfortunately, using `--parallel` limits terminal output such that only discovered URLs are shown. No amount of `-v`'s will help you here. I imagine this isn't too big of a deal, as folks that need `--parallel` probably aren't sitting there watching the output... 🙃 + +Example Command: +``` +cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail +``` + +Resuling Process List (illustrative): +``` +\_ target/debug/feroxbuster --stdin --parallel 10 + \_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-one + \_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-two + \_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-three + \_ ... + \_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-ten +``` + ## 🧐 Comparison w/ Similar Tools There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc... @@ -947,6 +975,7 @@ few of the use-cases in which feroxbuster may be a better fit: | hide progress bars or be silent (or some variation) (`v2.0.0`) | ✔ | ✔ | ✔ | | automatically tune scans based on errors/403s/429s (`v2.1.0`) | ✔ | | | | automatically stop scans based on errors/403s/429s (`v2.1.0`) | ✔ | | ✔ | +| run scans in parallel (1 process per target) (`v2.2.0`) | ✔ | | | | **huge** number of other options | | | ✔ | Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I diff --git a/ferox-config.toml.example b/ferox-config.toml.example index 1ef346f..756b161 100644 --- a/ferox-config.toml.example +++ b/ferox-config.toml.example @@ -16,6 +16,7 @@ # replay_proxy = "http://127.0.0.1:8081" # replay_codes = [200, 302] # verbosity = 1 +# parallel = 8 # scan_limit = 6 # rate_limit = 250 # quiet = true diff --git a/shell_completions/_feroxbuster b/shell_completions/_feroxbuster index 97ac1e4..bf493c2 100644 --- a/shell_completions/_feroxbuster +++ b/shell_completions/_feroxbuster @@ -58,6 +58,7 @@ _feroxbuster() { '*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]' \ '-L+[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \ '--scan-limit=[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \ +'--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]' \ '(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]' \ '--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]' \ '(--silent)*-v[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \ diff --git a/shell_completions/_feroxbuster.ps1 b/shell_completions/_feroxbuster.ps1 index d6f1b61..ed29564 100644 --- a/shell_completions/_feroxbuster.ps1 +++ b/shell_completions/_feroxbuster.ps1 @@ -63,6 +63,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock { [CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)') [CompletionResult]::new('-L', 'L', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)') [CompletionResult]::new('--scan-limit', 'scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)') + [CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)') [CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)') [CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)') diff --git a/shell_completions/feroxbuster.bash b/shell_completions/feroxbuster.bash index 430c81c..a9d5b29 100644 --- a/shell_completions/feroxbuster.bash +++ b/shell_completions/feroxbuster.bash @@ -20,7 +20,7 @@ _feroxbuster() { case "${cmd}" in feroxbuster) - opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --rate-limit --time-limit " + opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit " if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -199,6 +199,10 @@ _feroxbuster() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --parallel) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; --rate-limit) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/shell_completions/feroxbuster.fish b/shell_completions/feroxbuster.fish index 9b41f17..7f315ce 100644 --- a/shell_completions/feroxbuster.fish +++ b/shell_completions/feroxbuster.fish @@ -21,6 +21,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -s N -l filter-lines -d 'Filt complete -c feroxbuster -n "__fish_use_subcommand" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)' complete -c feroxbuster -n "__fish_use_subcommand" -l filter-similar-to -d 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)' complete -c feroxbuster -n "__fish_use_subcommand" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)' +complete -c feroxbuster -n "__fish_use_subcommand" -l parallel -d 'Run parallel feroxbuster instances (one child process per url passed via stdin)' complete -c feroxbuster -n "__fish_use_subcommand" -l rate-limit -d 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)' complete -c feroxbuster -n "__fish_use_subcommand" -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)' complete -c feroxbuster -n "__fish_use_subcommand" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)' diff --git a/src/banner/container.rs b/src/banner/container.rs index 3d36d66..806081b 100644 --- a/src/banner/container.rs +++ b/src/banner/container.rs @@ -125,6 +125,9 @@ pub struct Banner { /// represents Configuration.rate_limit rate_limit: BannerEntry, + /// represents Configuration.parallel + parallel: BannerEntry, + /// represents Configuration.auto_tune auto_tune: BannerEntry, @@ -281,6 +284,7 @@ impl Banner { BannerEntry::new("ðŸĪŠ", "Filter Wildcards", &(!config.dont_filter).to_string()); let add_slash = BannerEntry::new("🊓", "Add Slash", &config.add_slash.to_string()); let time_limit = BannerEntry::new("🕖", "Time Limit", &config.time_limit); + let parallel = BannerEntry::new("ðŸ›Ī", "Parallel Scans", &config.parallel.to_string()); let rate_limit = BannerEntry::new("🚧", "Requests per Second", &config.rate_limit.to_string()); @@ -304,6 +308,7 @@ impl Banner { filter_line_count, filter_regex, extract_links, + parallel, json, queries, output, @@ -518,6 +523,10 @@ by Ben "epi" Risher {} ver: {}"#, writeln!(&mut writer, "{}", self.scan_limit)?; } + if config.parallel > 0 { + writeln!(&mut writer, "{}", self.parallel)?; + } + if config.rate_limit > 0 { writeln!(&mut writer, "{}", self.rate_limit)?; } diff --git a/src/config/container.rs b/src/config/container.rs index 55a8d75..bdae9ef 100644 --- a/src/config/container.rs +++ b/src/config/container.rs @@ -198,6 +198,10 @@ pub struct Configuration { #[serde(default)] pub scan_limit: usize, + /// Number of parallel scans permitted; a limit of 0 means no limit is imposed + #[serde(default)] + pub parallel: usize, + /// Number of requests per second permitted (per directory); a limit of 0 means no limit is imposed #[serde(default)] pub rate_limit: usize, @@ -280,6 +284,7 @@ impl Default for Configuration { json: false, verbosity: 0, scan_limit: 0, + parallel: 0, rate_limit: 0, add_slash: false, insecure: false, @@ -350,7 +355,8 @@ impl Configuration { /// - **dont_filter**: `false` (auto filter wildcard responses) /// - **depth**: `4` (maximum recursion depth) /// - **scan_limit**: `0` (no limit on concurrent scans imposed) - /// - **rate_limit**: `0` (no limit on concurrent scans imposed) + /// - **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) /// - **replay_proxy**: `None` (no limit on concurrent scans imposed) /// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html) @@ -486,6 +492,7 @@ impl Configuration { update_config_if_present!(&mut config.threads, args, "threads", usize); update_config_if_present!(&mut config.depth, args, "depth", usize); update_config_if_present!(&mut config.scan_limit, args, "scan_limit", usize); + update_config_if_present!(&mut config.parallel, args, "parallel", usize); update_config_if_present!(&mut config.rate_limit, args, "rate_limit", usize); update_config_if_present!(&mut config.wordlist, args, "wordlist", String); update_config_if_present!(&mut config.output, args, "output", String); @@ -793,6 +800,7 @@ impl Configuration { ); update_if_not_default!(&mut conf.dont_filter, new.dont_filter, false); update_if_not_default!(&mut conf.scan_limit, new.scan_limit, 0); + update_if_not_default!(&mut conf.parallel, new.parallel, 0); update_if_not_default!(&mut conf.rate_limit, new.rate_limit, 0); update_if_not_default!(&mut conf.replay_proxy, new.replay_proxy, ""); update_if_not_default!(&mut conf.debug_log, new.debug_log, ""); diff --git a/src/config/tests.rs b/src/config/tests.rs index 6b8cd73..88e1b20 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -20,6 +20,7 @@ fn setup_config_test() -> Configuration { auto_bail = true verbosity = 1 scan_limit = 6 + parallel = 14 rate_limit = 250 time_limit = "10m" output = "/some/otherpath" @@ -146,6 +147,13 @@ fn config_reads_scan_limit() { assert_eq!(config.scan_limit, 6); } +#[test] +/// parse the test config and see that the value parsed is correct +fn config_reads_parallel() { + let config = setup_config_test(); + assert_eq!(config.parallel, 14); +} + #[test] /// parse the test config and see that the value parsed is correct fn config_reads_rate_limit() { diff --git a/src/main.rs b/src/main.rs index 27ee7f7..e71c071 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,19 @@ use std::{ collections::HashSet, + env::args, fs::File, io::{stderr, BufRead, BufReader}, + ops::Index, + process::Command, sync::{atomic::Ordering, Arc}, }; use anyhow::{bail, Context, Result}; use futures::StreamExt; -use tokio::{io, sync::oneshot}; +use tokio::{ + io, + sync::{oneshot, Semaphore}, +}; use tokio_util::codec::{FramedRead, LinesCodec}; use feroxbuster::{ @@ -26,6 +32,13 @@ use feroxbuster::{ }; #[cfg(not(target_os = "windows"))] use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT}; +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + /// Limits the number of parallel scans active at any given time when using --parallel + static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0); +} /// Create a HashSet of Strings from the given wordlist then stores it inside an Arc fn get_unique_words_from_wordlist(path: &str) -> Result>> { @@ -226,6 +239,72 @@ async fn wrapped_main(config: Arc) -> Result<()> { } }; + // --parallel branch + if config.parallel > 0 { + log::trace!("enter: parallel branch"); + + PARALLEL_LIMITER.add_permits(config.parallel); + + let invocation = args(); + + let para_regex = + Regex::new("--stdin|-q|--quiet|--silent|--verbosity|-v|-vv|-vvv|-vvvv").unwrap(); + + // remove stdin since only the original process will process targets + // remove quiet and silent so we can force silent later to normalize output + let mut original = invocation + .filter(|s| !para_regex.is_match(s)) + .collect::>(); + + original.push("--silent".to_string()); // only output modifier allowed + + // we need remove --parallel from command line so we don't hit this branch over and over + // but we must remove --parallel N manually; the filter above never sees --parallel and the + // value passed to it at the same time, so can't filter them out in one pass + + // unwrap is fine, as it has to be in the args for us to be in this code branch + let parallel_index = original.iter().position(|s| *s == "--parallel").unwrap(); + + // remove --parallel + original.remove(parallel_index); + + // remove N passed to --parallel (it's the same index again since everything shifts + // from removing --parallel) + original.remove(parallel_index); + + // unvalidated targets fresh from stdin, just spawn children and let them do all checks + for target in targets { + // add the current target to the provided command + let mut cloned = original.clone(); + cloned.push("-u".to_string()); + cloned.push(target); + + let bin = cloned.index(0).to_owned(); // user's path to feroxbuster + let args = cloned.index(1..).to_vec(); // and args + + let permit = PARALLEL_LIMITER.acquire().await?; + + log::debug!("parallel exec: {} {}", bin, args.join(" ")); + + tokio::task::spawn_blocking(move || { + let result = Command::new(bin) + .args(&args) + .spawn() + .expect("failed to spawn a child process") + .wait() + .expect("child process errored during execution"); + + drop(permit); + result + }); + } + + clean_up(handles, tasks).await?; + + log::trace!("exit: parallel branch && wrapped main"); + return Ok(()); + } + if matches!(config.output_level, OutputLevel::Default) { // only print banner if output level is default (no banner on --quiet|--silent) let std_stderr = stderr(); // std::io::stderr diff --git a/src/parser.rs b/src/parser.rs index 6c28dc0..0b2a12e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -348,6 +348,14 @@ pub fn initialize() -> App<'static, 'static> { .takes_value(true) .help("Limit total number of concurrent scans (default: 0, i.e. no limit)") ) + .arg( + Arg::with_name("parallel") + .long("parallel") + .value_name("PARALLEL_SCANS") + .takes_value(true) + .requires("stdin") + .help("Run parallel feroxbuster instances (one child process per url passed via stdin)") + ) .arg( Arg::with_name("rate_limit") .long("rate-limit") diff --git a/src/scan_manager/tests.rs b/src/scan_manager/tests.rs index 6cc630e..113f380 100644 --- a/src/scan_manager/tests.rs +++ b/src/scan_manager/tests.rs @@ -383,7 +383,7 @@ fn feroxstates_feroxserialize_implementation() { let json_state = ferox_state.as_json().unwrap(); let expected = format!( - r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#, + r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#, saved_id, VERSION ); println!("{}\n{}", expected, json_state); diff --git a/tests/test_banner.rs b/tests/test_banner.rs index 3890274..3fb4de6 100644 --- a/tests/test_banner.rs +++ b/tests/test_banner.rs @@ -948,3 +948,27 @@ fn banner_doesnt_print_when_quiet() { .and(predicate::str::contains("User-Agent").not()), ); } + +#[test] +/// test allows non-existent wordlist to trigger the banner printing to stderr +/// expect to see nothing as --parallel forces --silent to be true +fn banner_prints_parallel() { + Command::cargo_bin("feroxbuster") + .unwrap() + .arg("--stdin") + .arg("--parallel") + .arg("4316") + .assert() + .success() + .stderr( + predicate::str::contains("─┮─") + .not() + .and(predicate::str::contains("Target Url").not()) + .and(predicate::str::contains("Parallel Scans").not()) + .and(predicate::str::contains("Threads").not()) + .and(predicate::str::contains("Wordlist").not()) + .and(predicate::str::contains("Status Codes").not()) + .and(predicate::str::contains("Timeout (secs)").not()) + .and(predicate::str::contains("User-Agent").not()), + ); +} diff --git a/tests/test_main.rs b/tests/test_main.rs index 80ebe80..a632c8e 100644 --- a/tests/test_main.rs +++ b/tests/test_main.rs @@ -1,8 +1,9 @@ mod utils; use assert_cmd::Command; use httpmock::Method::GET; -use httpmock::MockServer; +use httpmock::{MockServer, Regex}; use predicates::prelude::*; +use std::fs::read_to_string; use utils::{setup_tmp_directory, teardown_tmp_directory}; #[test] @@ -89,3 +90,66 @@ fn main_use_empty_stdin_targets() -> Result<(), Box> { Ok(()) } + +#[test] +/// send three targets over stdin, expect parallel to spawn children and each child config to show +/// up in the output file +fn main_parallel_spawns_children() -> Result<(), Box> { + let t1 = MockServer::start(); + let t2 = MockServer::start(); + let t3 = MockServer::start(); + + let words = [ + String::from("LICENSE"), + String::from("stuff"), + String::from("things"), + String::from("mostuff"), + String::from("mothings"), + ]; + let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?; + let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?; + let (tgt_tmp_dir, targets) = + setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?; + + Command::cargo_bin("feroxbuster") + .unwrap() + .arg("--stdin") + .arg("--parallel") + .arg("2") + .arg("-vvvv") + .arg("--debug-log") + .arg(outfile.as_os_str()) + .arg("--wordlist") + .arg(wordlist.as_os_str()) + .pipe_stdin(targets) + .unwrap() + .assert() + .success() + .stderr( + predicate::str::contains("Could not connect to any target provided") + .and(predicate::str::contains("Target Url")) + .not(), // no target url found + ); + + let contents = read_to_string(outfile).unwrap(); + println!("contents: {}", contents); + + assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch + + // DBG 0.007 feroxbuster parallel exec: target/debug/feroxbuster + // --debug-log /tmp/.tmpAjRts6/output-file --wordlist /tmp/.tmpS4CKKq/wordlist + // --silent -u http://127.0.0.1:41979/ + let r1 = Regex::new(&format!("parallel exec:.*-u {}", t1.url("/"))).unwrap(); + let r2 = Regex::new(&format!("parallel exec:.*-u {}", t2.url("/"))).unwrap(); + let r3 = Regex::new(&format!("parallel exec:.*-u {}", t3.url("/"))).unwrap(); + + assert!(r1.is_match(&contents)); // all 3 were spawned + assert!(r2.is_match(&contents)); + assert!(r3.is_match(&contents)); + + teardown_tmp_directory(word_tmp_dir); + teardown_tmp_directory(tgt_tmp_dir); + teardown_tmp_directory(output_dir); + + Ok(()) +}