Compare commits

..

42 Commits

Author SHA1 Message Date
epi
3881789879 removed unnecessary test 2020-11-21 07:55:10 -06:00
epi
df19c63901 fixed up getting the progress bar in scanner 2020-11-21 07:36:43 -06:00
epi
582ce9ed8d bumped version to 1.6.3 2020-11-21 06:40:42 -06:00
epi
697a1cf715 added spinner back in; updated comments with what to change for 107 finalization 2020-11-20 20:39:18 -06:00
epi
8eec5ce1d9 even more tests! 2020-11-20 19:53:45 -06:00
epi
c08180872e added more tests for scan_manager 2020-11-20 19:34:23 -06:00
epi
f8b18576aa added param to pause function for testability 2020-11-20 16:09:40 -06:00
epi
46a471c8a7 added param to pause function for testability 2020-11-20 16:09:30 -06:00
epi
1b1190582a added a test for display scans 2020-11-20 15:38:47 -06:00
epi
addf867f59 fixed the hanging issue; cleaned up 2020-11-20 14:03:23 -06:00
epi
4ef95ec246 Merge branch 'master' into FEATURE-107-cancel-scans-from-paused-state 2020-11-19 19:39:02 -06:00
epi
b48445f714 cargo fmt 2020-11-19 15:16:13 -06:00
epi
dc10a56c79 Merge pull request #132 from epi052/reimplement-size-filters-using-filter-trait
Reimplement size-based filters using FeroxFilter trait
2020-11-19 14:44:47 -06:00
epi
b1b9ea71de made tests more specific 2020-11-19 14:25:25 -06:00
epi
3c41573db2 added more tests 2020-11-19 13:53:49 -06:00
epi
9929104adc increased test coverage in filters 2020-11-19 13:06:52 -06:00
epi
eca26b73c5 updated clippy command in pull request template 2020-11-19 11:19:56 -06:00
epi
5464ae4ddd added scanner::initialize, all filters reimplemented 2020-11-19 10:50:09 -06:00
epi
1c9a42c9ea removed prints from tests 2020-11-19 08:57:33 -06:00
epi
805f02ad2d incremental save; a transmitter isnt being dropped 2020-11-19 06:45:08 -06:00
epi
880e884dea clippy and fmt 2020-11-17 20:17:24 -06:00
epi
fd4a8d87a6 Merge branch 'master' into FEATURE-107-cancel-scans-from-paused-state 2020-11-17 19:57:07 -06:00
epi
922014cb9b added 3 new filters to represent size,words,lines 2020-11-17 19:55:46 -06:00
epi
db88e168b2 bumped version to 1.6.2 2020-11-17 19:22:23 -06:00
epi
85cba02b81 Merge pull request #127 from epi052/125-add-url-from-whence-we-came
reduced log output by a lot; added redirection location on error
2020-11-17 18:59:06 -06:00
epi
a93fe91459 fixed a comment that didnt make sense 2020-11-17 18:57:19 -06:00
epi
4b811a42b9 tidied up a few report strings and fixed a clippy issue 2020-11-17 17:22:03 -06:00
epi
678d371ca4 Merge branch 'master' into 125-add-url-from-whence-we-came 2020-11-17 16:45:14 -06:00
epi
4f31ed1847 ran cargo fmt 2020-11-17 10:44:33 -06:00
epi
a7185f4262 changed optional body read to true 2020-11-17 10:30:43 -06:00
epi
a78f6b714d bumped version to 1.6.1 2020-11-17 10:30:27 -06:00
epi
771a9556f1 cleaned up make_request, ran fmt 2020-11-15 06:39:02 -06:00
epi
48e53be244 cleaned up make_request, ran fmt 2020-11-15 06:37:39 -06:00
epi
23279eb1ed removed debug message that just reported the url 2020-11-14 15:49:42 -06:00
epi
88260e0b04 toned down logging 2020-11-14 15:34:18 -06:00
epi
e6f7a00ba0 initial guess at grabbing the correct info 2020-11-14 10:11:05 -06:00
epi
2b7392735a added pretty print of current scans 2020-11-13 17:17:36 -06:00
epi
b00a47e5e5 moved functions related to scan management into their own module 2020-11-12 15:00:49 -06:00
epi
171238b71d Merge branch 'master' into FEATURE-107-cancel-scans-from-paused-state 2020-11-12 07:00:01 -06:00
epi
d0a6c61de2 pre master merge 2020-11-12 06:54:09 -06:00
epi
a2e13ea71a added call to new scanner::initialize function 2020-11-10 07:16:31 -06:00
epi
169d6c16fd added normalize_url to utils 2020-11-10 06:18:20 -06:00
14 changed files with 1098 additions and 318 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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() {

View File

@@ -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
);
}
}

View File

@@ -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);
}
}

View File

@@ -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`

View File

@@ -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(),
);

View File

@@ -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,

View 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")

View File

@@ -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
View 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
}
}

View File

@@ -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);
}
}

View File

@@ -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::*;

View File

@@ -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);
}