Init commit for

This commit is contained in:
MD-Levitan
2021-12-30 19:18:51 +03:00
parent 03e0d0092d
commit 4d49401a96
20 changed files with 153 additions and 33 deletions

2
Cargo.lock generated
View File

@@ -602,7 +602,7 @@ dependencies = [
[[package]]
name = "feroxbuster"
version = "2.4.2"
version = "2.5.0"
dependencies = [
"anyhow",
"assert_cmd",

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.4.2"
version = "2.5.0"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"

View File

@@ -32,6 +32,7 @@
# insecure = true
# extensions = ["php", "html"]
# methods = ["GET", "POST"]
# data = [11, 12, 13, 14, 15]
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
# regex_denylist = ["/deny.*"]
# no_recursion = true

View File

@@ -43,6 +43,7 @@ _feroxbuster() {
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*-m+[HTTP request method(s) to search for (default: \[GET\])]' \
'*--methods=[HTTP request method(s) to search for (default: \[GET\])]' \
'--data=[HTTP Body data (default: empty)]' \
'*--dont-scan=[URL(s) or Regex Pattern(s) to exclude from recursion/scans]' \
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \

View File

@@ -48,6 +48,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'HTTP request method(s) to search for (default: [GET])')
[CompletionResult]::new('--methods', 'methods', [CompletionResultType]::ParameterName, 'HTTP request method(s) to search for (default: [GET])')
[CompletionResult]::new('--data', 'data', [CompletionResultType]::ParameterName, 'HTTP Body data (default: empty)')
[CompletionResult]::new('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) or Regex Pattern(s) to exclude from recursion/scans')
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')

View File

@@ -20,7 +20,7 @@ _feroxbuster() {
case "${cmd}" in
feroxbuster)
opts=" -v -q -D -A -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -m -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --random-agent --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 --methods --dont-scan --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
opts=" -v -q -D -A -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -m -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --random-agent --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 --methods --data --dont-scan --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
@@ -139,6 +139,10 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--data)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dont-scan)
COMPREPLY=($(compgen -f "${cur}"))
return 0

View File

@@ -13,6 +13,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_use_subcommand" -s m -l methods -d 'HTTP request method(s) to search for (default: [GET])'
complete -c feroxbuster -n "__fish_use_subcommand" -l data -d 'HTTP Body data (default: empty)'
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) or Regex Pattern(s) to exclude from recursion/scans'
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'

View File

@@ -101,6 +101,9 @@ pub struct Banner {
/// represents Configuration.methods
methods: BannerEntry,
/// represents Configuration.data
data: BannerEntry,
/// represents Configuration.insecure
insecure: BannerEntry,
@@ -310,6 +313,11 @@ impl Banner {
"HTTP methods",
&format!("[{}]", config.methods.join(", ")),
);
let data = BannerEntry::new(
"💣",
"HTTP Body data",
&String::from_utf8_lossy(&config.data),
);
let insecure = BannerEntry::new("🔓", "Insecure", &config.insecure.to_string());
let redirects = BannerEntry::new("📍", "Follow Redirects", &config.redirects.to_string());
let dont_filter =
@@ -348,6 +356,7 @@ impl Banner {
debug_log,
extensions,
methods,
data,
insecure,
dont_filter,
redirects,
@@ -404,7 +413,7 @@ by Ben "epi" Risher {} ver: {}"#,
let api_url = Url::parse(url)?;
let result = logged_request(&api_url, DEFAULT_METHOD, handles.clone()).await?;
let result = logged_request(&api_url, DEFAULT_METHOD, None, handles.clone()).await?;
let body = result.text().await?;
let json_response: Value = serde_json::from_str(&body)?;
@@ -538,6 +547,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.methods)?;
}
if !config.data.is_empty() {
writeln!(&mut writer, "{}", self.data)?;
}
if config.insecure {
writeln!(&mut writer, "{}", self.insecure)?;
}

View File

@@ -173,9 +173,13 @@ pub struct Configuration {
/// HTTP requests methods(s) to search for
/// To make this serialisible will store as String
#[serde(default)]
#[serde(default = "methods")]
pub methods: Vec<String>,
/// HTTP Body data to send during request
#[serde(default)]
pub data: Vec<u8>,
/// HTTP headers to be used in each request
#[serde(default)]
pub headers: HashMap<String, String>,
@@ -322,6 +326,7 @@ impl Default for Configuration {
queries: Vec::new(),
extensions: Vec::new(),
methods: methods,
data: Vec::new(),
filter_size: Vec::new(),
filter_regex: Vec::new(),
url_denylist: Vec::new(),
@@ -365,6 +370,7 @@ impl Configuration {
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
/// - **extensions**: `None`
/// - **methods**: [`DEFAULT_METHOD`]
/// - **data**: `None`
/// - **url_denylist**: `None`
/// - **regex_denylist**: `None`
/// - **filter_size**: `None`
@@ -576,6 +582,10 @@ impl Configuration {
.collect();
}
if let Some(url) = args.value_of("data") {
config.data = url.as_bytes().to_vec();
}
if args.is_present("stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
@@ -854,6 +864,7 @@ impl Configuration {
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
if !new.regex_denylist.is_empty() {
// cant use the update_if_not_default macro due to the following error

View File

@@ -32,6 +32,7 @@ fn setup_config_test() -> Configuration {
insecure = true
extensions = ["html", "php", "js"]
methods = ["GET", "PUT", "DELETE"]
data = [31, 32, 33, 34]
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
regex_denylist = ["/deny.*"]
headers = {stuff = "things", mostuff = "mothings"}
@@ -97,7 +98,8 @@ fn default_configuration() {
assert_eq!(config.queries, Vec::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.methods, Vec::<String>::new());
assert_eq!(config.methods, vec!["GET"]);
assert_eq!(config.data, Vec::<u8>::new());
assert_eq!(config.url_denylist, Vec::<Url>::new());
assert_eq!(config.filter_regex, Vec::<String>::new());
assert_eq!(config.filter_similar, Vec::<String>::new());
@@ -303,6 +305,14 @@ fn config_reads_methods() {
assert_eq!(config.methods, vec!["GET", "PUT", "DELETE"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_data() {
let config = setup_config_test();
assert_eq!(config.data, vec![31, 32, 33, 34]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_regex_denylist() {

View File

@@ -12,7 +12,7 @@ use crate::{
statistics::StatField::ResourcesDiscovered,
traits::FeroxSerialize,
utils::{ferox_print, fmt_err, make_request, open_file, write_to},
CommandReceiver, CommandSender, Joiner, DEFAULT_METHOD,
CommandReceiver, CommandSender, Joiner
};
use std::sync::Arc;
@@ -214,7 +214,8 @@ impl TermOutHandler {
make_request(
self.config.replay_client.as_ref().unwrap(),
resp.url(),
DEFAULT_METHOD,
&resp.method().as_str(),
None,
self.config.output_level,
&self.config,
tx_stats.clone(),

View File

@@ -325,7 +325,8 @@ impl<'a> Extractor<'a> {
}
// make the request and store the response
let new_response = logged_request(&new_url, DEFAULT_METHOD, self.handles.clone()).await?;
let new_response =
logged_request(&new_url, DEFAULT_METHOD, None, self.handles.clone()).await?;
let new_ferox_response = FeroxResponse::from(
new_response,
@@ -411,6 +412,7 @@ impl<'a> Extractor<'a> {
&client,
&url,
DEFAULT_METHOD,
None,
self.handles.config.output_level,
&self.handles.config,
self.handles.stats.tx.clone(),

View File

@@ -224,6 +224,7 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
&client,
&url,
DEFAULT_METHOD,
None,
OutputLevel::Default,
&config,
tx_stats.clone(),

View File

@@ -72,7 +72,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
let url = skip_fail!(Url::parse(similarity_filter));
// attempt to request the given url
let resp = skip_fail!(logged_request(&url, DEFAULT_METHOD, handles.clone()).await);
let resp = skip_fail!(logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await);
// if successful, create a filter based on the response's body
let fr = FeroxResponse::from(

View File

@@ -122,6 +122,7 @@ fn wildcard_should_filter_when_static_wildcard_found() {
size: 83,
dynamic: 0,
dont_filter: false,
method: "GET".to_owned(),
};
assert!(filter.should_filter_response(&resp));
@@ -152,6 +153,7 @@ fn wildcard_should_filter_when_dynamic_wildcard_found() {
size: 0,
dynamic: 59, // content-length - 5 (len('stuff'))
dont_filter: false,
method: "GET".to_owned(),
};
println!("resp: {:?}: filter: {:?}", resp, filter);

View File

@@ -90,11 +90,16 @@ impl HeuristicTests {
return Ok(0);
}
let data = match self.handles.config.data.is_empty() {
true => None,
false => Some(&self.handles.config.data[..]),
};
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
for method in self.handles.config.methods.iter() {
let ferox_response = self
.make_wildcard_request(&ferox_url, method.as_str(), 1)
.make_wildcard_request(&ferox_url, method.as_str(), data, 1)
.await?;
// found a wildcard response
@@ -111,7 +116,7 @@ impl HeuristicTests {
// content length of wildcard is non-zero, perform additional tests:
// make a second request, with a known-sized (64) longer request
let resp_two = self
.make_wildcard_request(&ferox_url, method.as_str(), 3)
.make_wildcard_request(&ferox_url, method.as_str(), data, 3)
.await?;
let wc2_length = resp_two.content_length();
@@ -161,6 +166,7 @@ impl HeuristicTests {
&self,
target: &FeroxUrl,
method: &str,
data: Option<&[u8]>,
length: usize,
) -> Result<FeroxResponse> {
log::trace!("enter: make_wildcard_request({}, {})", target, length);
@@ -176,8 +182,13 @@ impl HeuristicTests {
let nonexistent_url = target.format(&unique_str, slash)?;
let response =
logged_request(&nonexistent_url.to_owned(), method, self.handles.clone()).await?;
let response = logged_request(
&nonexistent_url.to_owned(),
method,
data,
self.handles.clone(),
)
.await?;
if self
.handles
@@ -235,7 +246,7 @@ impl HeuristicTests {
let url = FeroxUrl::from_string(target_url, self.handles.clone());
let request = skip_fail!(url.format("", None));
let result = logged_request(&request, DEFAULT_METHOD, self.handles.clone()).await;
let result = logged_request(&request, DEFAULT_METHOD, None, self.handles.clone()).await;
match result {
Ok(_) => {

View File

@@ -239,6 +239,15 @@ pub fn initialize() -> App<'static, 'static> {
"HTTP request method(s) to search for (default: [GET])",
),
)
.arg(
Arg::with_name("data")
.long("data")
.value_name("DATA")
.takes_value(true)
.help(
"HTTP Body data (default: empty)",
),
)
.arg(
Arg::with_name("url_denylist")
.long("dont-scan")

View File

@@ -23,7 +23,7 @@ use crate::{
statistics::{StatError::Other, StatField::TotalExpected},
url::FeroxUrl,
utils::logged_request,
HIGH_ERROR_RATIO,
HIGH_ERROR_RATIO
};
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
@@ -41,6 +41,9 @@ pub(super) struct Requester {
/// HTTP methods what will be use for scan
methods: Vec<String>,
/// HTTP body data
data: Vec<u8>,
/// limits requests per second if present
rate_limiter: RwLock<Option<LeakyBucket>>,
@@ -89,6 +92,7 @@ impl Requester {
target_url: scanner.target_url.to_owned(),
tuning_lock: Mutex::new(0),
methods: scanner.handles.config.methods.clone(),
data: scanner.handles.config.data.clone(),
})
}
@@ -335,7 +339,13 @@ impl Requester {
continue;
}
let response = logged_request(&url, method.as_str(), self.handles.clone()).await?;
let response = logged_request(
&url,
method.as_str(),
Some(&self.data[..]),
self.handles.clone(),
)
.await?;
if (should_tune || self.handles.config.auto_bail)
&& !atomic_load!(self.policy_data.cooling_down, Ordering::SeqCst)
@@ -444,6 +454,7 @@ mod tests {
filters,
scan_manager::{ScanOrder, ScanType},
statistics::StatError,
DEFAULT_METHOD
};
use super::*;
@@ -590,7 +601,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -619,7 +630,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -645,7 +656,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -686,7 +697,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -742,7 +753,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: req_clone,
target_url: "http://one/one/stuff.php".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -777,7 +788,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://one/one/stuff.php".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -800,7 +811,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
};
@@ -824,7 +835,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
});
@@ -855,7 +866,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
@@ -894,7 +905,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
@@ -931,7 +942,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
@@ -960,7 +971,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
@@ -1004,7 +1015,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
@@ -1048,7 +1059,7 @@ mod tests {
tuning_lock: Mutex::new(0),
ferox_scan: scan.clone(),
target_url: "http://localhost".to_string(),
methods: vec![DEFAULT_METHOD.to_owned()],
methods: vec![DEFAULT_METHOD.to_owned()],data: vec![],
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoTune, 4),
};

View File

@@ -95,12 +95,17 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
/// wrapper for make_request used to pass error/response codes to FeroxScans for per-scan stats
/// tracking of information related to auto-tune/bail
pub async fn logged_request(url: &Url, method: &str, handles: Arc<Handles>) -> Result<Response> {
pub async fn logged_request(
url: &Url,
method: &str,
data: Option<&[u8]>,
handles: Arc<Handles>,
) -> Result<Response> {
let client = &handles.config.client;
let level = handles.config.output_level;
let tx_stats = handles.stats.tx.clone();
let response = make_request(client, url, method, level, &handles.config, tx_stats).await;
let response = make_request(client, url, method, data, level, &handles.config, tx_stats).await;
let scans = handles.ferox_scans()?;
match response {
@@ -126,6 +131,7 @@ pub async fn make_request(
client: &Client,
url: &Url,
method: &str,
data: Option<&[u8]>,
output_level: OutputLevel,
config: &Configuration,
tx_stats: UnboundedSender<Command>,
@@ -138,6 +144,10 @@ pub async fn make_request(
);
let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned());
if let Some(body_data) = data {
//TODO: Find the way how to improve this block
request = request.body(body_data.to_vec());
}
if config.random_agent {
let index = unsafe {

View File

@@ -1060,3 +1060,34 @@ fn banner_prints_methods() {
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + data body
fn banner_prints_data() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-m")
.arg("PUT")
.arg("--methods")
.arg("POST")
.arg("--data")
.arg("some_data")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Target Url"))
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("HTTP Body data"))
.and(predicate::str::contains("some_data"))
.and(predicate::str::contains("─┴─")),
);
}