mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-30 11:21:13 -03:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3881789879 | ||
|
|
df19c63901 | ||
|
|
582ce9ed8d | ||
|
|
697a1cf715 | ||
|
|
8eec5ce1d9 | ||
|
|
c08180872e | ||
|
|
f8b18576aa | ||
|
|
46a471c8a7 | ||
|
|
1b1190582a | ||
|
|
addf867f59 | ||
|
|
4ef95ec246 | ||
|
|
b48445f714 | ||
|
|
dc10a56c79 | ||
|
|
b1b9ea71de | ||
|
|
3c41573db2 | ||
|
|
9929104adc | ||
|
|
eca26b73c5 | ||
|
|
5464ae4ddd | ||
|
|
1c9a42c9ea | ||
|
|
805f02ad2d | ||
|
|
880e884dea | ||
|
|
fd4a8d87a6 | ||
|
|
922014cb9b | ||
|
|
db88e168b2 | ||
|
|
85cba02b81 | ||
|
|
a93fe91459 | ||
|
|
4b811a42b9 | ||
|
|
678d371ca4 | ||
|
|
4f31ed1847 | ||
|
|
a7185f4262 | ||
|
|
a78f6b714d | ||
|
|
771a9556f1 | ||
|
|
48e53be244 | ||
|
|
23279eb1ed | ||
|
|
88260e0b04 | ||
|
|
e6f7a00ba0 | ||
|
|
2b7392735a | ||
|
|
b00a47e5e5 | ||
|
|
171238b71d | ||
|
|
d0a6c61de2 | ||
|
|
a2e13ea71a | ||
|
|
169d6c16fd |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -11,7 +11,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
|
||||
|
||||
## Static analysis checks
|
||||
- [ ] All rust files are formatted using `cargo fmt`
|
||||
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap`
|
||||
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::deref_addrof`
|
||||
- [ ] All existing tests pass
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "1.6.0"
|
||||
version = "1.6.3"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
@@ -32,31 +32,33 @@ pub fn initialize(
|
||||
.default_headers(header_map)
|
||||
.redirect(policy);
|
||||
|
||||
let client = if proxy.is_some() && !proxy.unwrap().is_empty() {
|
||||
match Proxy::all(proxy.unwrap()) {
|
||||
Ok(proxy_obj) => client.proxy(proxy_obj),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {} Could not add proxy ({:?}) to Client configuration",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Client::initialize"),
|
||||
proxy
|
||||
);
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Client::initialize"),
|
||||
e
|
||||
);
|
||||
let client = match proxy {
|
||||
// a proxy is specified, need to add it to the client
|
||||
Some(some_proxy) => {
|
||||
if !some_proxy.is_empty() {
|
||||
// it's not an empty string
|
||||
match Proxy::all(some_proxy) {
|
||||
Ok(proxy_obj) => client.proxy(proxy_obj),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Client::initialize"),
|
||||
e
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
panic!();
|
||||
#[cfg(not(test))]
|
||||
exit(1);
|
||||
#[cfg(test)]
|
||||
panic!();
|
||||
#[cfg(not(test))]
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client // Some("") was used?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client
|
||||
// no proxy specified
|
||||
None => client,
|
||||
};
|
||||
|
||||
match client.build() {
|
||||
|
||||
153
src/filters.rs
153
src/filters.rs
@@ -53,7 +53,7 @@ impl FeroxFilter for WildcardFilter {
|
||||
/// Examine size, dynamic, and content_len to determine whether or not the response received
|
||||
/// is a wildcard response and therefore should be filtered out
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
||||
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||
|
||||
// quick return if dont_filter is set
|
||||
if CONFIGURATION.dont_filter {
|
||||
@@ -114,7 +114,7 @@ pub struct StatusCodeFilter {
|
||||
impl FeroxFilter for StatusCodeFilter {
|
||||
/// Check `filter_code` against what was passed in via -C|--filter-status
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
||||
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||
|
||||
if response.status().as_u16() == self.filter_code {
|
||||
log::debug!(
|
||||
@@ -140,3 +140,152 @@ impl FeroxFilter for StatusCodeFilter {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
|
||||
/// in a Response body; specified using -N|--filter-lines
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct LinesFilter {
|
||||
/// Number of lines in a Response's body that should be filtered
|
||||
pub line_count: usize,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for LinesFilter
|
||||
impl FeroxFilter for LinesFilter {
|
||||
/// Check `line_count` against what was passed in via -N|--filter-lines
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||
|
||||
let result = response.line_count() == self.line_count;
|
||||
|
||||
log::trace!("exit: should_filter_response -> {}", result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Compare one LinesFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
|
||||
/// in a Response body; specified using -W|--filter-words
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct WordsFilter {
|
||||
/// Number of words in a Response's body that should be filtered
|
||||
pub word_count: usize,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for WordsFilter
|
||||
impl FeroxFilter for WordsFilter {
|
||||
/// Check `word_count` against what was passed in via -W|--filter-words
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||
|
||||
let result = response.word_count() == self.word_count;
|
||||
|
||||
log::trace!("exit: should_filter_response -> {}", result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Compare one WordsFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
|
||||
/// Response body; specified using -S|--filter-size
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct SizeFilter {
|
||||
/// Overall length of a Response's body that should be filtered
|
||||
pub content_length: u64,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for SizeFilter
|
||||
impl FeroxFilter for SizeFilter {
|
||||
/// Check `content_length` against what was passed in via -S|--filter-size
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||
|
||||
let result = response.content_length() == self.content_length;
|
||||
|
||||
log::trace!("exit: should_filter_response -> {}", result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Compare one SizeFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn lines_filter_as_any() {
|
||||
let filter = LinesFilter { line_count: 1 };
|
||||
|
||||
assert_eq!(filter.line_count, 1);
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<LinesFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn words_filter_as_any() {
|
||||
let filter = WordsFilter { word_count: 1 };
|
||||
|
||||
assert_eq!(filter.word_count, 1);
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<WordsFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn size_filter_as_any() {
|
||||
let filter = SizeFilter { content_length: 1 };
|
||||
|
||||
assert_eq!(filter.content_length, 1);
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<SizeFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn status_code_filter_as_any() {
|
||||
let filter = StatusCodeFilter { filter_code: 200 };
|
||||
|
||||
assert_eq!(filter.filter_code, 200);
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<StatusCodeFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,10 @@ pub async fn wildcard_test(
|
||||
|
||||
if !CONFIGURATION.quiet {
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
||||
"{} {:>8}l {:>8}w {:>8}c Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
ferox_response.line_count(),
|
||||
ferox_response.word_count(),
|
||||
wildcard.dynamic,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length - url_len).cyan(),
|
||||
@@ -110,8 +112,10 @@ pub async fn wildcard_test(
|
||||
|
||||
if !CONFIGURATION.quiet {
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
||||
"{} {:>8}l {:>8}w {:>8}c Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
ferox_response.line_count(),
|
||||
ferox_response.word_count(),
|
||||
wc_length,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length).cyan(),
|
||||
@@ -235,7 +239,7 @@ async fn make_wildcard_request(
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: make_wildcard_request -> {:?}", ferox_response);
|
||||
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
|
||||
return Some(ferox_response);
|
||||
}
|
||||
}
|
||||
|
||||
19
src/lib.rs
19
src/lib.rs
@@ -8,13 +8,11 @@ pub mod logger;
|
||||
pub mod parser;
|
||||
pub mod progress;
|
||||
pub mod reporter;
|
||||
pub mod scan_manager;
|
||||
pub mod scanner;
|
||||
pub mod utils;
|
||||
|
||||
use reqwest::{
|
||||
header::HeaderMap,
|
||||
{Response, StatusCode, Url},
|
||||
};
|
||||
use reqwest::{header::HeaderMap, Response, StatusCode, Url};
|
||||
use std::{error, fmt};
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
@@ -103,6 +101,19 @@ pub struct FeroxResponse {
|
||||
headers: HeaderMap,
|
||||
}
|
||||
|
||||
/// Implement Display for FeroxResponse
|
||||
impl fmt::Display for FeroxResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"FeroxResponse {{ url: {}, status: {}, content-length: {} }}",
|
||||
self.url(),
|
||||
self.status(),
|
||||
self.content_length()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `FeroxResponse` implementation
|
||||
impl FeroxResponse {
|
||||
/// Get the `StatusCode` of this `FeroxResponse`
|
||||
|
||||
@@ -19,8 +19,8 @@ pub fn initialize(verbosity: u8) {
|
||||
0 => (),
|
||||
1 => env::set_var("RUST_LOG", "warn"),
|
||||
2 => env::set_var("RUST_LOG", "info"),
|
||||
3 => env::set_var("RUST_LOG", "debug,hyper=info,reqwest=info"),
|
||||
_ => env::set_var("RUST_LOG", "trace,hyper=info,reqwest=info"),
|
||||
3 => env::set_var("RUST_LOG", "feroxbuster=debug,info"),
|
||||
_ => env::set_var("RUST_LOG", "feroxbuster=trace,info"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,10 @@ pub fn initialize(verbosity: u8) {
|
||||
};
|
||||
|
||||
let msg = format!(
|
||||
"{} {:10.03} {}\n",
|
||||
"{} {:10.03} {} {}\n",
|
||||
style(level_name).bg(level_color).black(),
|
||||
style(t).dim(),
|
||||
record.target(),
|
||||
style(record.args()).dim(),
|
||||
);
|
||||
|
||||
|
||||
15
src/main.rs
15
src/main.rs
@@ -3,7 +3,8 @@ use feroxbuster::{
|
||||
banner,
|
||||
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
heuristics, logger, reporter,
|
||||
scanner::{scan_url, PAUSE_SCAN},
|
||||
scan_manager::PAUSE_SCAN,
|
||||
scanner::{self, scan_url},
|
||||
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
||||
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
||||
};
|
||||
@@ -112,6 +113,16 @@ async fn scan(
|
||||
return Err(Box::new(err));
|
||||
}
|
||||
|
||||
scanner::initialize(
|
||||
words.len(),
|
||||
CONFIGURATION.scan_limit,
|
||||
&CONFIGURATION.extensions,
|
||||
&CONFIGURATION.filter_status,
|
||||
&CONFIGURATION.filter_line_count,
|
||||
&CONFIGURATION.filter_word_count,
|
||||
&CONFIGURATION.filter_size,
|
||||
);
|
||||
|
||||
let mut tasks = vec![];
|
||||
|
||||
for target in targets {
|
||||
@@ -243,7 +254,7 @@ async fn clean_up(
|
||||
save_output: bool,
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: clean_up({:?}, {:?}, {:?}, {:?}, {}",
|
||||
"enter: clean_up({:?}, {:?}, {:?}, {:?}, {})",
|
||||
tx_term,
|
||||
term_handle,
|
||||
tx_file,
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.long("verbosity")
|
||||
.takes_value(false)
|
||||
.multiple(true)
|
||||
.help("Increase verbosity level (use -vv or more for greater effect)"),
|
||||
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("proxy")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use crate::utils::{ferox_print, make_request, status_colorizer};
|
||||
use crate::utils::{create_report_string, ferox_print, make_request};
|
||||
use crate::{FeroxChannel, FeroxResponse};
|
||||
use console::strip_ansi_codes;
|
||||
use std::io::Write;
|
||||
@@ -95,23 +95,13 @@ async fn spawn_terminal_reporter(
|
||||
log::trace!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) {
|
||||
let report = if CONFIGURATION.quiet {
|
||||
// -q used, just need the url
|
||||
format!("{}\n", resp.url())
|
||||
} else {
|
||||
// normal printing with status and size
|
||||
let status = status_colorizer(&resp.status().as_str());
|
||||
format!(
|
||||
// example output
|
||||
// 200 3280 https://localhost.com/FAQ
|
||||
"{} {:>8}l {:>8}w {:>8}c {}\n",
|
||||
status,
|
||||
resp.line_count(),
|
||||
resp.word_count(),
|
||||
resp.content_length(),
|
||||
resp.url()
|
||||
)
|
||||
};
|
||||
let report = create_report_string(
|
||||
resp.status().as_str(),
|
||||
&resp.line_count().to_string(),
|
||||
&resp.word_count().to_string(),
|
||||
&resp.content_length().to_string(),
|
||||
&resp.url().to_string(),
|
||||
);
|
||||
|
||||
// print to stdout
|
||||
ferox_print(&report, &PROGRESS_PRINTER);
|
||||
|
||||
531
src/scan_manager.rs
Normal file
531
src/scan_manager.rs
Normal file
@@ -0,0 +1,531 @@
|
||||
use crate::{config::PROGRESS_PRINTER, progress, scanner::NUMBER_OF_REQUESTS, SLEEP_DURATION};
|
||||
use console::style;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
fmt,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use std::{
|
||||
io::{stderr, Write},
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
};
|
||||
use tokio::{task::JoinHandle, time};
|
||||
use uuid::Uuid;
|
||||
|
||||
lazy_static! {
|
||||
/// A clock spinner protected with a RwLock to allow for a single thread to use at a time
|
||||
// todo remove this when issue #107 is resolved
|
||||
static ref SINGLE_SPINNER: RwLock<ProgressBar> = RwLock::new(get_single_spinner());
|
||||
}
|
||||
|
||||
/// Single atomic number that gets incremented once, used to track first thread to interact with
|
||||
/// when pausing a scan
|
||||
static INTERACTIVE_BARRIER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not a scan should pause or resume
|
||||
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Simple enum used to flag a `FeroxScan` as likely a directory or file
|
||||
#[derive(Debug)]
|
||||
pub enum ScanType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
/// Struct to hold scan-related state
|
||||
///
|
||||
/// The purpose of this container is to open up the pathway to aborting currently running tasks and
|
||||
/// serialization of all scan state into a state file in order to resume scans that were cut short
|
||||
#[derive(Debug)]
|
||||
pub struct FeroxScan {
|
||||
/// UUID that uniquely ID's the scan
|
||||
pub id: String,
|
||||
|
||||
/// The URL that to be scanned
|
||||
pub url: String,
|
||||
|
||||
/// The type of scan
|
||||
pub scan_type: ScanType,
|
||||
|
||||
/// Whether or not this scan has completed
|
||||
pub complete: bool,
|
||||
|
||||
/// The spawned tokio task performing this scan
|
||||
pub task: Option<JoinHandle<()>>,
|
||||
|
||||
/// The progress bar associated with this scan
|
||||
pub progress_bar: Option<ProgressBar>,
|
||||
}
|
||||
|
||||
/// Implementation of FeroxScan
|
||||
impl FeroxScan {
|
||||
/// Stop a currently running scan
|
||||
pub fn abort(&self) {
|
||||
self.stop_progress_bar();
|
||||
|
||||
if let Some(_task) = &self.task {
|
||||
// task.abort(); todo uncomment once upgraded to tokio 0.3
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a default FeroxScan, populates ID with a new UUID
|
||||
fn default() -> Self {
|
||||
let new_id = Uuid::new_v4().to_simple().to_string();
|
||||
|
||||
FeroxScan {
|
||||
id: new_id,
|
||||
task: None,
|
||||
complete: false,
|
||||
url: String::new(),
|
||||
progress_bar: None,
|
||||
scan_type: ScanType::File,
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper to call .finish on the scan's progress bar
|
||||
fn stop_progress_bar(&self) {
|
||||
if let Some(pb) = &self.progress_bar {
|
||||
pb.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper get a progress bar
|
||||
pub fn progress_bar(&mut self) -> ProgressBar {
|
||||
if let Some(pb) = &self.progress_bar {
|
||||
pb.clone()
|
||||
} else {
|
||||
let num_requests = NUMBER_OF_REQUESTS.load(Ordering::Relaxed);
|
||||
let pb = progress::add_bar(&self.url, num_requests, false);
|
||||
|
||||
pb.reset_elapsed();
|
||||
|
||||
self.progress_bar = Some(pb.clone());
|
||||
|
||||
pb
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a URL and ProgressBar, create a new FeroxScan, wrap it in an Arc and return it
|
||||
pub fn new(url: &str, scan_type: ScanType, pb: Option<ProgressBar>) -> Arc<Mutex<Self>> {
|
||||
let mut me = Self::default();
|
||||
|
||||
me.url = url.to_string();
|
||||
me.scan_type = scan_type;
|
||||
me.progress_bar = pb;
|
||||
|
||||
Arc::new(Mutex::new(me))
|
||||
}
|
||||
|
||||
/// Mark the scan as complete and stop the scan's progress bar
|
||||
pub fn finish(&mut self) {
|
||||
self.complete = true;
|
||||
self.stop_progress_bar();
|
||||
}
|
||||
}
|
||||
|
||||
/// Display implementation
|
||||
impl fmt::Display for FeroxScan {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let complete = if self.complete {
|
||||
style("complete").green()
|
||||
} else {
|
||||
style("incomplete").red()
|
||||
};
|
||||
|
||||
write!(f, "{:10} {}", complete, self.url)
|
||||
}
|
||||
}
|
||||
|
||||
/// PartialEq implementation; uses FeroxScan.id for comparison
|
||||
impl PartialEq for FeroxScan {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Container around a locked hashset of `FeroxScan`s, adds wrappers for insertion and searching
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeroxScans {
|
||||
/// Internal structure: locked hashset of `FeroxScan`s
|
||||
pub scans: Mutex<Vec<Arc<Mutex<FeroxScan>>>>,
|
||||
}
|
||||
|
||||
/// Implementation of `FeroxScans`
|
||||
impl FeroxScans {
|
||||
/// Add a `FeroxScan` to the internal container
|
||||
///
|
||||
/// If the internal container did NOT contain the scan, true is returned; else false
|
||||
pub fn insert(&self, scan: Arc<Mutex<FeroxScan>>) -> bool {
|
||||
let sentry = match scan.lock() {
|
||||
Ok(locked_scan) => {
|
||||
// If the container did contain the scan, set sentry to false
|
||||
// If the container did not contain the scan, set sentry to true
|
||||
!self.contains(&locked_scan.url)
|
||||
}
|
||||
Err(e) => {
|
||||
// poisoned lock
|
||||
log::error!("FeroxScan's ({:?}) mutex is poisoned: {}", self, e);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if sentry {
|
||||
// can't update the internal container while the scan itself is locked, so first
|
||||
// lock the scan and check the container for the scan's presence, then add if
|
||||
// not found
|
||||
match self.scans.lock() {
|
||||
Ok(mut scans) => {
|
||||
scans.push(scan);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("FeroxScans' container's mutex is poisoned: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sentry
|
||||
}
|
||||
|
||||
/// Simple check for whether or not a FeroxScan is contained within the inner container based
|
||||
/// on the given URL
|
||||
pub fn contains(&self, url: &str) -> bool {
|
||||
match self.scans.lock() {
|
||||
Ok(scans) => {
|
||||
for scan in scans.iter() {
|
||||
if let Ok(locked_scan) = scan.lock() {
|
||||
if locked_scan.url == url {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("FeroxScans' container's mutex is poisoned: {}", e);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find and return a `FeroxScan` based on the given URL
|
||||
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<Mutex<FeroxScan>>> {
|
||||
if let Ok(scans) = self.scans.lock() {
|
||||
for scan in scans.iter() {
|
||||
if let Ok(locked_scan) = scan.lock() {
|
||||
if locked_scan.url == url {
|
||||
return Some(scan.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Print all FeroxScans of type Directory
|
||||
///
|
||||
/// Example:
|
||||
/// 0: complete https://10.129.45.20
|
||||
/// 9: complete https://10.129.45.20/images
|
||||
/// 10: complete https://10.129.45.20/assets
|
||||
pub fn display_scans(&self) {
|
||||
if let Ok(scans) = self.scans.lock() {
|
||||
for (i, scan) in scans.iter().enumerate() {
|
||||
if let Ok(unlocked_scan) = scan.lock() {
|
||||
match unlocked_scan.scan_type {
|
||||
ScanType::Directory => {
|
||||
PROGRESS_PRINTER.println(format!("{:3}: {}", i, unlocked_scan));
|
||||
}
|
||||
ScanType::File => {
|
||||
// we're only interested in displaying directory scans, as those are
|
||||
// the only ones that make sense to be stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forced the calling thread into a busy loop
|
||||
///
|
||||
/// Every `SLEEP_DURATION` milliseconds, the function examines the result stored in `PAUSE_SCAN`
|
||||
///
|
||||
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
||||
/// loop
|
||||
pub async fn pause(&self, get_user_input: bool) {
|
||||
// function uses tokio::time, not std
|
||||
|
||||
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
||||
// concurrent scans rose when SLEEP_DURATION was set to 500, using that as the default for now
|
||||
let mut interval = time::interval(time::Duration::from_millis(SLEEP_DURATION));
|
||||
|
||||
// ignore any error returned
|
||||
let _ = stderr().flush();
|
||||
|
||||
if INTERACTIVE_BARRIER.load(Ordering::Relaxed) == 0 {
|
||||
INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
if get_user_input {
|
||||
self.display_scans();
|
||||
|
||||
let mut user_input = String::new();
|
||||
std::io::stdin().read_line(&mut user_input).unwrap();
|
||||
// todo actual logic for parsing user input in a way that allows for
|
||||
// calling .abort on the scan retrieved based on the input (issue #107)
|
||||
}
|
||||
}
|
||||
|
||||
if SINGLE_SPINNER.read().unwrap().is_finished() {
|
||||
// todo remove this when issue #107 is resolved
|
||||
|
||||
// in order to not leave draw artifacts laying around in the terminal, we call
|
||||
// finish_and_clear on the progress bar when resuming scans. For this reason, we need to
|
||||
// check if the spinner is finished, and repopulate the RwLock with a new spinner if
|
||||
// necessary
|
||||
if let Ok(mut guard) = SINGLE_SPINNER.write() {
|
||||
*guard = get_single_spinner();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
spinner.enable_steady_tick(120);
|
||||
}
|
||||
|
||||
loop {
|
||||
// first tick happens immediately, all others wait the specified duration
|
||||
interval.tick().await;
|
||||
|
||||
if !PAUSE_SCAN.load(Ordering::Acquire) {
|
||||
// PAUSE_SCAN is false, so we can exit the busy loop
|
||||
|
||||
if INTERACTIVE_BARRIER.load(Ordering::Relaxed) == 1 {
|
||||
INTERACTIVE_BARRIER.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
// todo remove this when issue #107 is resolved
|
||||
spinner.finish_and_clear();
|
||||
}
|
||||
|
||||
let _ = stderr().flush();
|
||||
|
||||
log::trace!("exit: pause_scan");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// Also return a reference to the new `FeroxScan`
|
||||
fn add_scan(&self, url: &str, scan_type: ScanType) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||
let bar = match scan_type {
|
||||
ScanType::Directory => {
|
||||
let progress_bar =
|
||||
progress::add_bar(&url, NUMBER_OF_REQUESTS.load(Ordering::Relaxed), false);
|
||||
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
Some(progress_bar)
|
||||
}
|
||||
ScanType::File => None,
|
||||
};
|
||||
|
||||
let ferox_scan = FeroxScan::new(&url, scan_type, bar);
|
||||
|
||||
// If the set did not contain the scan, true is returned.
|
||||
// If the set did contain the scan, false is returned.
|
||||
let response = self.insert(ferox_scan.clone());
|
||||
|
||||
(response, ferox_scan)
|
||||
}
|
||||
|
||||
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a Directory Scan
|
||||
///
|
||||
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
|
||||
///
|
||||
/// Also return a reference to the new `FeroxScan`
|
||||
pub fn add_directory_scan(&self, url: &str) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||
self.add_scan(&url, ScanType::Directory)
|
||||
}
|
||||
|
||||
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
||||
///
|
||||
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
|
||||
///
|
||||
/// Also return a reference to the new `FeroxScan`
|
||||
pub fn add_file_scan(&self, url: &str) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||
self.add_scan(&url, ScanType::File)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a clock spinner, used when scans are paused
|
||||
// todo remove this when issue #107 is resolved
|
||||
fn get_single_spinner() -> ProgressBar {
|
||||
log::trace!("enter: get_single_spinner");
|
||||
|
||||
let spinner = ProgressBar::new_spinner().with_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.tick_strings(&[
|
||||
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
|
||||
])
|
||||
.template(&format!(
|
||||
"\t-= All Scans {{spinner}} {} =-",
|
||||
style("Paused").red()
|
||||
)),
|
||||
);
|
||||
|
||||
log::trace!("exit: get_single_spinner -> {:?}", spinner);
|
||||
spinner
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
/// test that get_single_spinner returns the correct spinner
|
||||
// todo remove this when issue #107 is resolved
|
||||
fn scanner_get_single_spinner_returns_spinner() {
|
||||
let spinner = get_single_spinner();
|
||||
assert!(!spinner.is_finished());
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// tests that pause_scan pauses execution and releases execution when PAUSE_SCAN is toggled
|
||||
/// the spinner used during the test has had .finish_and_clear called on it, meaning that
|
||||
/// a new one will be created, taking the if branch within the function
|
||||
async fn scanner_pause_scan_with_finished_spinner() {
|
||||
let now = time::Instant::now();
|
||||
let urls = FeroxScans::default();
|
||||
|
||||
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
||||
|
||||
let expected = time::Duration::from_secs(2);
|
||||
|
||||
tokio::spawn(async move {
|
||||
time::delay_for(expected).await;
|
||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
urls.pause(false).await;
|
||||
|
||||
assert!(now.elapsed() > expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add an unknown url to the hashset, expect true
|
||||
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
||||
let urls = FeroxScans::default();
|
||||
let url = "http://unknown_url";
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory);
|
||||
assert_eq!(result, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, with a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url() {
|
||||
let urls = FeroxScans::default();
|
||||
let pb = ProgressBar::new(1);
|
||||
let url = "http://unknown_url/";
|
||||
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory);
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// abort should call stop_progress_bar, marking it as finished
|
||||
fn abort_stops_progress_bar() {
|
||||
let pb = ProgressBar::new(1);
|
||||
let url = "http://unknown_url/";
|
||||
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||
|
||||
assert_eq!(
|
||||
scan.lock()
|
||||
.unwrap()
|
||||
.progress_bar
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished(),
|
||||
false
|
||||
);
|
||||
|
||||
scan.lock().unwrap().abort();
|
||||
|
||||
assert_eq!(
|
||||
scan.lock()
|
||||
.unwrap()
|
||||
.progress_bar
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, without a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
||||
let urls = FeroxScans::default();
|
||||
let url = "http://unknown_url";
|
||||
let scan = FeroxScan::new(url, ScanType::File, None);
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::File);
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just increasing coverage, no real expectations
|
||||
fn call_display_scans() {
|
||||
let urls = FeroxScans::default();
|
||||
let pb = ProgressBar::new(1);
|
||||
let pb_two = ProgressBar::new(2);
|
||||
let url = "http://unknown_url/";
|
||||
let url_two = "http://unknown_url/fa";
|
||||
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||
let scan_two = FeroxScan::new(url_two, ScanType::Directory, Some(pb_two));
|
||||
|
||||
scan_two.lock().unwrap().finish(); // one complete, one incomplete
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
|
||||
urls.display_scans();
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// ensure that PartialEq compares FeroxScan.id fields
|
||||
fn partial_eq_compares_the_id_field() {
|
||||
let url = "http://unknown_url/";
|
||||
let scan = FeroxScan::new(url, ScanType::Directory, None);
|
||||
let scan_two = FeroxScan::new(url, ScanType::Directory, None);
|
||||
|
||||
assert!(!scan.lock().unwrap().eq(&scan_two.lock().unwrap()));
|
||||
|
||||
scan_two.lock().unwrap().id = scan.lock().unwrap().id.clone();
|
||||
|
||||
assert!(scan.lock().unwrap().eq(&scan_two.lock().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// show that a new progress bar is created if one doesn't exist
|
||||
fn ferox_scan_get_progress_bar_when_none_is_set() {
|
||||
let mut scan = FeroxScan::default();
|
||||
|
||||
assert!(scan.progress_bar.is_none()); // no pb exists
|
||||
|
||||
let pb = scan.progress_bar();
|
||||
|
||||
assert!(scan.progress_bar.is_some()); // new pb created
|
||||
assert!(!pb.is_finished()) // not finished
|
||||
}
|
||||
}
|
||||
382
src/scanner.rs
382
src/scanner.rs
@@ -1,25 +1,25 @@
|
||||
use crate::{
|
||||
config::CONFIGURATION,
|
||||
extractor::get_links,
|
||||
filters::{FeroxFilter, StatusCodeFilter, WildcardFilter},
|
||||
heuristics, progress,
|
||||
filters::{
|
||||
FeroxFilter, LinesFilter, SizeFilter, StatusCodeFilter, WildcardFilter, WordsFilter,
|
||||
},
|
||||
heuristics,
|
||||
scan_manager::{FeroxScans, PAUSE_SCAN},
|
||||
utils::{format_url, get_current_depth, make_request},
|
||||
FeroxChannel, FeroxResponse, SLEEP_DURATION,
|
||||
FeroxChannel, FeroxResponse,
|
||||
};
|
||||
use console::style;
|
||||
use futures::{
|
||||
future::{BoxFuture, FutureExt},
|
||||
stream, StreamExt,
|
||||
};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::Url;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryInto,
|
||||
io::{stderr, Write},
|
||||
ops::Deref,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
sync::atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tokio::{
|
||||
@@ -28,21 +28,17 @@ use tokio::{
|
||||
Semaphore,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time,
|
||||
};
|
||||
|
||||
/// Single atomic number that gets incremented once, used to track first scan vs. all others
|
||||
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not a scan should pause or resume
|
||||
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
||||
/// Single atomic number that gets holds the number of requests to be sent per directory scanned
|
||||
pub static NUMBER_OF_REQUESTS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
lazy_static! {
|
||||
/// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication
|
||||
static ref SCANNED_URLS: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
||||
|
||||
/// A clock spinner protected with a RwLock to allow for a single thread to use at a time
|
||||
static ref SINGLE_SPINNER: RwLock<ProgressBar> = RwLock::new(get_single_spinner());
|
||||
pub static ref SCANNED_URLS: FeroxScans = FeroxScans::default();
|
||||
|
||||
/// Vector of implementors of the FeroxFilter trait
|
||||
static ref FILTERS: Arc<RwLock<Vec<Box<dyn FeroxFilter>>>> = Arc::new(RwLock::new(Vec::<Box<dyn FeroxFilter>>::new()));
|
||||
@@ -51,101 +47,6 @@ lazy_static! {
|
||||
static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit);
|
||||
}
|
||||
|
||||
/// Return a clock spinner, used when scans are paused
|
||||
fn get_single_spinner() -> ProgressBar {
|
||||
log::trace!("enter: get_single_spinner");
|
||||
|
||||
let spinner = ProgressBar::new_spinner().with_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.tick_strings(&[
|
||||
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
|
||||
])
|
||||
.template(&format!(
|
||||
"\t-= All Scans {{spinner}} {} =-",
|
||||
style("Paused").red()
|
||||
)),
|
||||
);
|
||||
|
||||
log::trace!("exit: get_single_spinner -> {:?}", spinner);
|
||||
spinner
|
||||
}
|
||||
|
||||
/// Forced the calling thread into a busy loop
|
||||
///
|
||||
/// Every `SLEEP_DURATION` milliseconds, the function examines the result stored in `PAUSE_SCAN`
|
||||
///
|
||||
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
||||
/// loop
|
||||
async fn pause_scan() {
|
||||
log::trace!("enter: pause_scan");
|
||||
// function uses tokio::time, not std
|
||||
|
||||
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
||||
// concurrent scans rose when SLEEP_DURATION was set to 500, using that as the default for now
|
||||
let mut interval = time::interval(time::Duration::from_millis(SLEEP_DURATION));
|
||||
|
||||
// ignore any error returned
|
||||
let _ = stderr().flush();
|
||||
|
||||
if SINGLE_SPINNER.read().unwrap().is_finished() {
|
||||
// in order to not leave draw artifacts laying around in the terminal, we call
|
||||
// finish_and_clear on the progress bar when resuming scans. For this reason, we need to
|
||||
// check if the spinner is finished, and repopulate the RwLock with a new spinner if
|
||||
// necessary
|
||||
if let Ok(mut guard) = SINGLE_SPINNER.write() {
|
||||
*guard = get_single_spinner();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
spinner.enable_steady_tick(120);
|
||||
}
|
||||
|
||||
loop {
|
||||
// first tick happens immediately, all others wait the specified duration
|
||||
interval.tick().await;
|
||||
|
||||
if !PAUSE_SCAN.load(Ordering::Acquire) {
|
||||
// PAUSE_SCAN is false, so we can exit the busy loop
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
spinner.finish_and_clear();
|
||||
}
|
||||
let _ = stderr().flush();
|
||||
log::trace!("exit: pause_scan");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given url to `SCANNED_URLS`
|
||||
///
|
||||
/// If `SCANNED_URLS` did not already contain the url, return true; otherwise return false
|
||||
fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock<HashSet<String>>) -> bool {
|
||||
log::trace!(
|
||||
"enter: add_url_to_list_of_scanned_urls({}, {:?})",
|
||||
resp,
|
||||
scanned_urls
|
||||
);
|
||||
|
||||
match scanned_urls.write() {
|
||||
// check new url against what's already been scanned
|
||||
Ok(mut urls) => {
|
||||
// If the set did not contain resp, true is returned.
|
||||
// If the set did contain resp, false is returned.
|
||||
let response = urls.insert(resp.to_string());
|
||||
|
||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> {}", response);
|
||||
response
|
||||
}
|
||||
Err(e) => {
|
||||
// poisoned lock
|
||||
log::error!("Set of scanned urls poisoned: {}", e);
|
||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> false");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given FeroxFilter to the given list of FeroxFilter implementors
|
||||
///
|
||||
/// If the given list did not already contain the filter, return true; otherwise return false
|
||||
@@ -205,7 +106,7 @@ fn spawn_recursion_handler(
|
||||
let mut scans = vec![];
|
||||
|
||||
while let Some(resp) = recursion_channel.recv().await {
|
||||
let unknown = add_url_to_list_of_scanned_urls(&resp, &SCANNED_URLS);
|
||||
let (unknown, _) = SCANNED_URLS.add_directory_scan(&resp);
|
||||
|
||||
if !unknown {
|
||||
// not unknown, i.e. we've seen the url before and don't need to scan again
|
||||
@@ -219,7 +120,7 @@ fn spawn_recursion_handler(
|
||||
let resp_clone = resp.clone();
|
||||
let list_clone = wordlist.clone();
|
||||
|
||||
scans.push(tokio::spawn(async move {
|
||||
let future = tokio::spawn(async move {
|
||||
scan_url(
|
||||
resp_clone.to_owned().as_str(),
|
||||
list_clone,
|
||||
@@ -228,7 +129,9 @@ fn spawn_recursion_handler(
|
||||
file_clone,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
});
|
||||
|
||||
scans.push(future);
|
||||
}
|
||||
scans
|
||||
}
|
||||
@@ -285,7 +188,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec<Url>
|
||||
/// handles 2xx and 3xx responses by either checking if the url ends with a / (2xx)
|
||||
/// or if the Location header is present and matches the base url + / (3xx)
|
||||
fn response_is_directory(response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: is_directory({:?})", response);
|
||||
log::trace!("enter: is_directory({})", response);
|
||||
|
||||
if response.status().is_redirection() {
|
||||
// status code is 3xx
|
||||
@@ -311,10 +214,7 @@ fn response_is_directory(response: &FeroxResponse) -> bool {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::debug!(
|
||||
"expected Location header, but none was found: {:?}",
|
||||
response
|
||||
);
|
||||
log::debug!("expected Location header, but none was found: {}", response);
|
||||
log::trace!("exit: is_directory -> false");
|
||||
return false;
|
||||
}
|
||||
@@ -370,7 +270,7 @@ async fn try_recursion(
|
||||
transmitter: UnboundedSender<String>,
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: try_recursion({:?}, {}, {:?})",
|
||||
"enter: try_recursion({}, {}, {:?})",
|
||||
response,
|
||||
base_depth,
|
||||
transmitter
|
||||
@@ -418,22 +318,6 @@ async fn try_recursion(
|
||||
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
|
||||
/// to the user or not.
|
||||
pub fn should_filter_response(response: &FeroxResponse) -> bool {
|
||||
if CONFIGURATION
|
||||
.filter_size
|
||||
.contains(&response.content_length())
|
||||
|| CONFIGURATION
|
||||
.filter_line_count
|
||||
.contains(&response.line_count())
|
||||
|| CONFIGURATION
|
||||
.filter_word_count
|
||||
.contains(&response.word_count())
|
||||
{
|
||||
// filtered value from --filter-size, size filters and wildcards are two separate filters
|
||||
// and are applied independently
|
||||
log::debug!("size filter: filtered out {}", response.url());
|
||||
return true;
|
||||
}
|
||||
|
||||
match FILTERS.read() {
|
||||
Ok(filters) => {
|
||||
for filter in filters.iter() {
|
||||
@@ -476,7 +360,7 @@ async fn make_requests(
|
||||
for url in urls {
|
||||
if let Ok(response) = make_request(&CONFIGURATION.client, &url).await {
|
||||
// response came back without error, convert it to FeroxResponse
|
||||
let ferox_response = FeroxResponse::from(response, CONFIGURATION.extract_links).await;
|
||||
let ferox_response = FeroxResponse::from(response, true).await;
|
||||
|
||||
// do recursion if appropriate
|
||||
if !CONFIGURATION.no_recursion {
|
||||
@@ -494,13 +378,6 @@ async fn make_requests(
|
||||
let new_links = get_links(&ferox_response).await;
|
||||
|
||||
for new_link in new_links {
|
||||
let unknown = add_url_to_list_of_scanned_urls(&new_link, &SCANNED_URLS);
|
||||
|
||||
if !unknown {
|
||||
// not unknown, i.e. we've seen the url before and don't need to scan again
|
||||
continue;
|
||||
}
|
||||
|
||||
// create a url based on the given command line options, continue on error
|
||||
let new_url = match format_url(
|
||||
&new_link,
|
||||
@@ -513,14 +390,18 @@ async fn make_requests(
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if SCANNED_URLS.get_scan_by_url(&new_url.to_string()).is_some() {
|
||||
//we've seen the url before and don't need to scan again
|
||||
continue;
|
||||
}
|
||||
|
||||
// make the request and store the response
|
||||
let new_response = match make_request(&CONFIGURATION.client, &new_url).await {
|
||||
Ok(resp) => resp,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let mut new_ferox_response =
|
||||
FeroxResponse::from(new_response, CONFIGURATION.extract_links).await;
|
||||
let mut new_ferox_response = FeroxResponse::from(new_response, true).await;
|
||||
|
||||
// filter if necessary
|
||||
if should_filter_response(&new_ferox_response) {
|
||||
@@ -529,11 +410,9 @@ async fn make_requests(
|
||||
|
||||
if new_ferox_response.is_file() {
|
||||
// very likely a file, simply request and report
|
||||
log::debug!(
|
||||
"Singular extraction: {} ({})",
|
||||
new_ferox_response.url(),
|
||||
new_ferox_response.status().as_str(),
|
||||
);
|
||||
log::debug!("Singular extraction: {}", new_ferox_response);
|
||||
|
||||
SCANNED_URLS.add_file_scan(&new_url.to_string());
|
||||
|
||||
send_report(report_chan.clone(), new_ferox_response);
|
||||
|
||||
@@ -541,11 +420,7 @@ async fn make_requests(
|
||||
}
|
||||
|
||||
if !CONFIGURATION.no_recursion {
|
||||
log::debug!(
|
||||
"Recursive extraction: {} ({})",
|
||||
new_ferox_response.url(),
|
||||
new_ferox_response.status().as_str()
|
||||
);
|
||||
log::debug!("Recursive extraction: {}", new_ferox_response);
|
||||
|
||||
if new_ferox_response.status().is_success()
|
||||
&& !new_ferox_response.url().as_str().ends_with('/')
|
||||
@@ -571,7 +446,7 @@ async fn make_requests(
|
||||
|
||||
/// Simple helper to send a `FeroxResponse` over the tx side of an `mpsc::unbounded_channel`
|
||||
fn send_report(report_sender: UnboundedSender<FeroxResponse>, response: FeroxResponse) {
|
||||
log::trace!("enter: send_report({:?}, {:?}", report_sender, response);
|
||||
log::trace!("enter: send_report({:?}, {}", report_sender, response);
|
||||
|
||||
match report_sender.send(response) {
|
||||
Ok(_) => {}
|
||||
@@ -606,31 +481,33 @@ pub async fn scan_url(
|
||||
|
||||
let (tx_dir, rx_dir): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
|
||||
let num_reqs_expected: u64 = if CONFIGURATION.extensions.is_empty() {
|
||||
wordlist.len().try_into().unwrap()
|
||||
} else {
|
||||
let total = wordlist.len() * (CONFIGURATION.extensions.len() + 1);
|
||||
total.try_into().unwrap()
|
||||
};
|
||||
|
||||
let progress_bar = progress::add_bar(&target_url, num_reqs_expected, false);
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
if CALL_COUNT.load(Ordering::Relaxed) == 0 {
|
||||
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// this protection allows us to add the first scanned url to SCANNED_URLS
|
||||
// from within the scan_url function instead of the recursion handler
|
||||
add_url_to_list_of_scanned_urls(&target_url, &SCANNED_URLS);
|
||||
|
||||
if CONFIGURATION.scan_limit == 0 {
|
||||
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
|
||||
// permit is tricky, so as a workaround, we'll add a ridiculous number of permits to
|
||||
// the semaphore (1,152,921,504,606,846,975 to be exact) and call that 'unlimited'
|
||||
SCAN_LIMITER.add_permits(usize::MAX >> 4);
|
||||
}
|
||||
SCANNED_URLS.add_directory_scan(&target_url);
|
||||
}
|
||||
|
||||
let ferox_scan = match SCANNED_URLS.get_scan_by_url(&target_url) {
|
||||
Some(scan) => scan,
|
||||
None => {
|
||||
log::error!(
|
||||
"Could not find FeroxScan associated with {}; this shouldn't happen... exiting",
|
||||
target_url
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let progress_bar = match ferox_scan.lock() {
|
||||
Ok(mut scan) => scan.progress_bar(),
|
||||
Err(e) => {
|
||||
log::error!("FeroxScan's ({:?}) mutex is poisoned: {}", ferox_scan, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// When acquire is called and the semaphore has remaining permits, the function immediately
|
||||
// returns a permit. However, if no remaining permits are available, acquire (asynchronously)
|
||||
// waits until an outstanding permit is dropped. At this point, the freed permit is assigned
|
||||
@@ -665,15 +542,6 @@ pub async fn scan_url(
|
||||
|
||||
add_filter_to_list_of_ferox_filters(filter, FILTERS.clone());
|
||||
|
||||
// add any status code filters to `FILTERS`
|
||||
for code_filter in &CONFIGURATION.filter_status {
|
||||
let filter = StatusCodeFilter {
|
||||
filter_code: *code_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
// producer tasks (mp of mpsc); responsible for making requests
|
||||
let producers = stream::iter(looping_words.deref().to_owned())
|
||||
.map(|word| {
|
||||
@@ -687,7 +555,9 @@ pub async fn scan_url(
|
||||
// for every word in the wordlist, check to see if PAUSE_SCAN is set to true
|
||||
// when true; enter a busy loop that only exits by setting PAUSE_SCAN back
|
||||
// to false
|
||||
pause_scan().await;
|
||||
|
||||
// todo change to true when issue #107 is resolved
|
||||
SCANNED_URLS.pause(false).await;
|
||||
}
|
||||
make_requests(&tgt, &word, base_depth, txd, txr).await
|
||||
}),
|
||||
@@ -713,7 +583,9 @@ pub async fn scan_url(
|
||||
// drop the current permit so the semaphore will allow another scan to proceed
|
||||
drop(permit);
|
||||
|
||||
progress_bar.finish();
|
||||
if let Ok(mut scan) = ferox_scan.lock() {
|
||||
scan.finish();
|
||||
}
|
||||
|
||||
// manually drop tx in order for the rx task's while loops to eval to false
|
||||
log::trace!("dropped recursion handler's transmitter");
|
||||
@@ -727,6 +599,84 @@ pub async fn scan_url(
|
||||
log::trace!("exit: scan_url");
|
||||
}
|
||||
|
||||
/// Perform steps necessary to run scans that only need to be performed once (warming up the
|
||||
/// engine, as it were)
|
||||
pub fn initialize(
|
||||
num_words: usize,
|
||||
scan_limit: usize,
|
||||
extensions: &[String],
|
||||
status_code_filters: &[u16],
|
||||
lines_filters: &[usize],
|
||||
words_filters: &[usize],
|
||||
size_filters: &[u64],
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: initialize({}, {}, {:?}, {:?}, {:?}, {:?}, {:?})",
|
||||
num_words,
|
||||
scan_limit,
|
||||
extensions,
|
||||
status_code_filters,
|
||||
lines_filters,
|
||||
words_filters,
|
||||
size_filters,
|
||||
);
|
||||
|
||||
// number of requests only needs to be calculated once, and then can be reused
|
||||
let num_reqs_expected: u64 = if extensions.is_empty() {
|
||||
num_words.try_into().unwrap()
|
||||
} else {
|
||||
let total = num_words * (extensions.len() + 1);
|
||||
total.try_into().unwrap()
|
||||
};
|
||||
|
||||
NUMBER_OF_REQUESTS.store(num_reqs_expected, Ordering::Relaxed);
|
||||
|
||||
// add any status code filters to `FILTERS` (-C|--filter-status)
|
||||
for code_filter in status_code_filters {
|
||||
let filter = StatusCodeFilter {
|
||||
filter_code: *code_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
// add any line count filters to `FILTERS` (-N|--filter-lines)
|
||||
for lines_filter in lines_filters {
|
||||
let filter = LinesFilter {
|
||||
line_count: *lines_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
// add any line count filters to `FILTERS` (-W|--filter-words)
|
||||
for words_filter in words_filters {
|
||||
let filter = WordsFilter {
|
||||
word_count: *words_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
// add any line count filters to `FILTERS` (-S|--filter-size)
|
||||
for size_filter in size_filters {
|
||||
let filter = SizeFilter {
|
||||
content_length: *size_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
if scan_limit == 0 {
|
||||
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
|
||||
// permit is tricky, so as a workaround, we'll add a ridiculous number of permits to
|
||||
// the semaphore (1,152,921,504,606,846,975 to be exact) and call that 'unlimited'
|
||||
SCAN_LIMITER.add_permits(usize::MAX >> 4);
|
||||
}
|
||||
|
||||
log::trace!("exit: initialize");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -824,68 +774,4 @@ mod tests {
|
||||
let result = reached_max_depth(&url, 0, 2);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add an unknown url to the hashset, expect true
|
||||
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url";
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, with a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url/";
|
||||
|
||||
assert_eq!(urls.write().unwrap().insert(url.to_string()), true);
|
||||
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, without a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url";
|
||||
|
||||
assert_eq!(
|
||||
urls.write()
|
||||
.unwrap()
|
||||
.insert("http://unknown_url".to_string()),
|
||||
true
|
||||
);
|
||||
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that get_single_spinner returns the correct spinner
|
||||
fn scanner_get_single_spinner_returns_spinner() {
|
||||
let spinner = get_single_spinner();
|
||||
assert!(!spinner.is_finished());
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// tests that pause_scan pauses execution and releases execution when PAUSE_SCAN is toggled
|
||||
/// the spinner used during the test has had .finish_and_clear called on it, meaning that
|
||||
/// a new one will be created, taking the if branch within the function
|
||||
async fn scanner_pause_scan_with_finished_spinner() {
|
||||
let now = time::Instant::now();
|
||||
|
||||
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
||||
SINGLE_SPINNER.write().unwrap().finish_and_clear();
|
||||
|
||||
let expected = time::Duration::from_secs(2);
|
||||
|
||||
tokio::spawn(async move {
|
||||
time::delay_for(expected).await;
|
||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
pause_scan().await;
|
||||
|
||||
assert!(now.elapsed() > expected);
|
||||
}
|
||||
}
|
||||
|
||||
74
src/utils.rs
74
src/utils.rs
@@ -1,8 +1,10 @@
|
||||
use crate::{FeroxError, FeroxResult};
|
||||
use crate::{
|
||||
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||
FeroxError, FeroxResult,
|
||||
};
|
||||
use console::{strip_ansi_codes, style, user_attended};
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::Url;
|
||||
use reqwest::{Client, Response};
|
||||
use reqwest::{Client, Response, Url};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
||||
use std::convert::TryInto;
|
||||
@@ -21,13 +23,7 @@ use std::convert::TryInto;
|
||||
pub fn get_current_depth(target: &str) -> usize {
|
||||
log::trace!("enter: get_current_depth({})", target);
|
||||
|
||||
let target = if !target.ends_with('/') {
|
||||
// target url doesn't end with a /, for the purposes of determining depth, we'll normalize
|
||||
// all urls to end in a / and then calculate accordingly
|
||||
format!("{}/", target)
|
||||
} else {
|
||||
String::from(target)
|
||||
};
|
||||
let target = normalize_url(target);
|
||||
|
||||
match Url::parse(&target) {
|
||||
Ok(url) => {
|
||||
@@ -90,8 +86,8 @@ pub fn get_url_path_length(url: &Url) -> u64 {
|
||||
|
||||
let path = url.path();
|
||||
|
||||
let segments = if path.starts_with('/') {
|
||||
path[1..].split_terminator('/')
|
||||
let segments = if let Some(split) = path.strip_prefix('/') {
|
||||
split.split_terminator('/')
|
||||
} else {
|
||||
log::trace!("exit: get_url_path_length -> 0");
|
||||
return 0;
|
||||
@@ -244,7 +240,6 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
||||
|
||||
match client.get(url.to_owned()).send().await {
|
||||
Ok(resp) => {
|
||||
log::debug!("requested Url: {}", resp.url());
|
||||
log::trace!("exit: make_request -> {:?}", resp);
|
||||
Ok(resp)
|
||||
}
|
||||
@@ -253,6 +248,19 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
||||
if e.to_string().contains("operation timed out") {
|
||||
// only warn for timeouts, while actual errors are still left as errors
|
||||
log::warn!("Error while making request: {}", e);
|
||||
} else if e.is_redirect() {
|
||||
if let Some(last_redirect) = e.url() {
|
||||
// get where we were headed (last_redirect) and where we came from (url)
|
||||
let fancy_message = format!("{} !=> {}", url, last_redirect);
|
||||
|
||||
let report = if let Some(msg_status) = e.status() {
|
||||
create_report_string(msg_status.as_str(), "-1", "-1", "-1", &fancy_message)
|
||||
} else {
|
||||
create_report_string("UNK", "-1", "-1", "-1", &fancy_message)
|
||||
};
|
||||
|
||||
ferox_print(&report, &PROGRESS_PRINTER)
|
||||
};
|
||||
} else {
|
||||
log::error!("Error while making request: {}", e);
|
||||
}
|
||||
@@ -261,6 +269,30 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create the standard line for output to file/terminal
|
||||
///
|
||||
/// example output:
|
||||
/// 200 127l 283w 4134c http://localhost/faq
|
||||
pub fn create_report_string(
|
||||
status: &str,
|
||||
line_count: &str,
|
||||
word_count: &str,
|
||||
content_length: &str,
|
||||
url: &str,
|
||||
) -> String {
|
||||
if CONFIGURATION.quiet {
|
||||
// -q used, just need the url
|
||||
format!("{}\n", url)
|
||||
} else {
|
||||
// normal printing with status and sizes
|
||||
let color_status = status_colorizer(status);
|
||||
format!(
|
||||
"{} {:>8}l {:>8}w {:>8}c {}\n",
|
||||
color_status, line_count, word_count, content_length, url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to set the soft limit for the RLIMIT_NOFILE resource
|
||||
///
|
||||
/// RLIMIT_NOFILE is the maximum number of file descriptors that can be opened by this process
|
||||
@@ -312,6 +344,22 @@ pub fn set_open_file_limit(limit: usize) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Simple helper to abstract away adding a forward-slash to a url if not present
|
||||
///
|
||||
/// used mostly for deduplication purposes and url state tracking
|
||||
pub fn normalize_url(url: &str) -> String {
|
||||
log::trace!("enter: normalize_url({})", url);
|
||||
|
||||
let normalized = if url.ends_with('/') {
|
||||
url.to_string()
|
||||
} else {
|
||||
format!("{}/", url)
|
||||
};
|
||||
|
||||
log::trace!("exit: normalize_url -> {}", normalized);
|
||||
normalized
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -55,3 +55,150 @@ fn filters_status_code_should_filter_response() {
|
||||
assert_eq!(mock_two.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// create a FeroxResponse that should elicit a true from
|
||||
/// LinesFilter::should_filter_response
|
||||
fn filters_lines_should_filter_response() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(302)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock_two = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/file.js")
|
||||
.return_status(200)
|
||||
.return_body("this is also a test of some import\nwith 2 lines, no less")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--filter-lines")
|
||||
.arg("2")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("302"))
|
||||
.and(predicate::str::contains("14"))
|
||||
.and(predicate::str::contains("/file.js"))
|
||||
.not()
|
||||
.and(predicate::str::contains("200"))
|
||||
.not()
|
||||
.and(predicate::str::contains("2l"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock_two.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// create a FeroxResponse that should elicit a true from
|
||||
/// WordsFilter::should_filter_response
|
||||
fn filters_words_should_filter_response() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(302)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock_two = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/file.js")
|
||||
.return_status(200)
|
||||
.return_body("this is also a test of some import\nwith 2 lines, no less")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--filter-words")
|
||||
.arg("13")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("302"))
|
||||
.and(predicate::str::contains("14"))
|
||||
.and(predicate::str::contains("/file.js"))
|
||||
.not()
|
||||
.and(predicate::str::contains("200"))
|
||||
.not()
|
||||
.and(predicate::str::contains("13w"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock_two.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// create a FeroxResponse that should elicit a true from
|
||||
/// SizeFilter::should_filter_response
|
||||
fn filters_size_should_filter_response() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(302)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock_two = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/file.js")
|
||||
.return_status(200)
|
||||
.return_body("this is also a test of some import\nwith 2 lines, no less")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--filter-size")
|
||||
.arg("56")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("302"))
|
||||
.and(predicate::str::contains("14"))
|
||||
.and(predicate::str::contains("/file.js"))
|
||||
.not()
|
||||
.and(predicate::str::contains("200"))
|
||||
.not()
|
||||
.and(predicate::str::contains("56c"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock_two.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user