diff --git a/Cargo.lock b/Cargo.lock index ee4ff42..82991fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,14 +411,14 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fd7173631a4e9e2ca8b32ae2fad58aab9843ea5aaf56642661937d87e28a3e" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio 0.7.14", + "mio", "parking_lot", "signal-hook", "signal-hook-mio", @@ -634,9 +634,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] @@ -1251,9 +1251,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" [[package]] name = "libnghttp2-sys" @@ -1344,19 +1344,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "mio" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - [[package]] name = "mio" version = "0.8.2" @@ -1746,9 +1733,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] @@ -1934,9 +1921,9 @@ dependencies = [ [[package]] name = "rlimit" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbcc6f9672090a4060a6de8d0fb2d4acd381363cda5b7049a26e2b32464f8ad" +checksum = "f7278a1ec8bfd4a4e07515c589f5ff7b309a373f987393aef44813d9dcf87aa3" dependencies = [ "libc", ] @@ -2123,7 +2110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio 0.7.14", + "mio", "signal-hook", ] @@ -2221,9 +2208,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", @@ -2359,7 +2346,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.2", + "mio", "num_cpus", "once_cell", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 1614ca8..cfee4a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,8 +44,8 @@ console = "0.15.0" openssl = { version = "0.10.38", features = ["vendored"] } dirs = "4.0.0" regex = "1.5.5" -crossterm = "0.23.1" -rlimit = "0.8.1" +crossterm = "0.23.2" +rlimit = "0.8.3" ctrlc = "3.2.1" fuzzyhash = "0.2.1" anyhow = "1.0.56" diff --git a/src/event_handlers/command.rs b/src/event_handlers/command.rs index 5c91172..9a131c4 100644 --- a/src/event_handlers/command.rs +++ b/src/event_handlers/command.rs @@ -43,6 +43,9 @@ pub enum Command { /// Add a `FeroxFilter` implementor to `FilterHandler`'s instance of `FeroxFilters` AddFilter(Box), + /// Remove a set of `FeroxFilter` implementors from `FeroxFilters` by index + RemoveFilters(Vec), + /// Send a `FeroxResponse` to the output handler for reporting Report(Box), diff --git a/src/event_handlers/filters.rs b/src/event_handlers/filters.rs index c723f03..f57c210 100644 --- a/src/event_handlers/filters.rs +++ b/src/event_handlers/filters.rs @@ -90,6 +90,7 @@ impl FiltersHandler { self.data.push(filter)?; } } + Command::RemoveFilters(mut indices) => self.data.remove(&mut indices), Command::Sync(sender) => { log::debug!("filters: {:?}", self); sender.send(true).unwrap_or_default(); @@ -128,7 +129,7 @@ mod tests { event_handle.send(Command::Sync(tx)).unwrap(); rx.await.unwrap(); - assert!(event_handle.data.filters.lock().unwrap().is_empty()); + assert!(event_handle.data.filters.read().unwrap().is_empty()); event_handle .send(Command::AddFilter(Box::new(WordsFilter { word_count: 1 }))) @@ -138,6 +139,6 @@ mod tests { event_handle.send(Command::Sync(tx)).unwrap(); rx.await.unwrap(); - assert_eq!(event_handle.data.filters.lock().unwrap().len(), 1); + assert_eq!(event_handle.data.filters.read().unwrap().len(), 1); } } diff --git a/src/filters/container.rs b/src/filters/container.rs index cb52b0a..f3f4cf2 100644 --- a/src/filters/container.rs +++ b/src/filters/container.rs @@ -1,4 +1,4 @@ -use std::sync::Mutex; +use std::sync::RwLock; use anyhow::Result; use serde::{ser::SerializeSeq, Serialize, Serializer}; @@ -17,14 +17,14 @@ use super::{ #[derive(Debug, Default)] pub struct FeroxFilters { /// collection of `FeroxFilters` - pub filters: Mutex>>, + pub filters: RwLock>>, } /// implementation of FeroxFilter collection impl FeroxFilters { /// add a single FeroxFilter to the collection pub fn push(&self, filter: Box) -> Result<()> { - if let Ok(mut guard) = self.filters.lock() { + if let Ok(mut guard) = self.filters.write() { if guard.contains(&filter) { return Ok(()); } @@ -34,6 +34,37 @@ impl FeroxFilters { Ok(()) } + /// remove items from the underlying collection by their index + /// + /// note: indexes passed in should be index-to-remove+1. This is built for the scan mgt menu + /// so indexes aren't 0-based whehn the user enters them. + /// + pub fn remove(&self, indices: &mut [usize]) { + // since we're removing by index, indices must be sorted and then reversed. + // this allows us to iterate over the collection from the rear, allowing any shifting + // of the vector to happen on sections that we no longer care about, as we're moving + // in the opposite direction + indices.sort_unstable(); + indices.reverse(); + + if let Ok(mut guard) = self.filters.write() { + for index in indices { + // numbering of the menu starts at 1, so we'll need to reduce the index by 1 + // to account for that. if they've provided 0 as an offset, we'll set the + // result to a gigantic number and skip it in the loop with a bounds check + let reduced_idx = index.checked_sub(1).unwrap_or(usize::MAX); + + // check if number provided is out of range + if reduced_idx >= guard.len() { + // usize can't be negative, just need to handle exceeding bounds + continue; + } + + guard.remove(reduced_idx); + } + } + } + /// 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( @@ -41,7 +72,7 @@ impl FeroxFilters { response: &FeroxResponse, tx_stats: CommandSender, ) -> bool { - if let Ok(filters) = self.filters.lock() { + if let Ok(filters) = self.filters.read() { for filter in filters.iter() { // wildcard.should_filter goes here if filter.should_filter_response(response) { @@ -63,7 +94,7 @@ impl Serialize for FeroxFilters { where S: Serializer, { - if let Ok(guard) = self.filters.lock() { + if let Ok(guard) = self.filters.read() { let mut seq = serializer.serialize_seq(Some(guard.len()))?; for filter in &*guard { diff --git a/src/filters/similarity.rs b/src/filters/similarity.rs index a9d8950..8da3f3b 100644 --- a/src/filters/similarity.rs +++ b/src/filters/similarity.rs @@ -10,6 +10,9 @@ pub struct SimilarityFilter { /// Percentage of similarity at which a page is determined to be a near-duplicate of another pub threshold: u32, + + /// Url originally requested for the similarity filter + pub original_url: String, } /// implementation of FeroxFilter for SimilarityFilter diff --git a/src/filters/tests.rs b/src/filters/tests.rs index 18a344c..f65258c 100644 --- a/src/filters/tests.rs +++ b/src/filters/tests.rs @@ -188,6 +188,7 @@ fn similarity_filter_is_accurate() { let mut filter = SimilarityFilter { hash: FuzzyHash::new("kitten").to_string(), threshold: 95, + original_url: "".to_string(), }; // kitten/sitting is 57% similar, so a threshold of 95 should not be filtered @@ -213,11 +214,13 @@ fn similarity_filter_as_any() { let filter = SimilarityFilter { hash: String::from("stuff"), threshold: 95, + original_url: "".to_string(), }; let filter2 = SimilarityFilter { hash: String::from("stuff"), threshold: 95, + original_url: "".to_string(), }; assert!(filter.box_eq(filter2.as_any())); @@ -228,3 +231,39 @@ fn similarity_filter_as_any() { filter ); } + +#[test] +/// test correctness of FeroxFilters::remove +fn remove_function_works_as_expected() { + let data = FeroxFilters::default(); + assert!(data.filters.read().unwrap().is_empty()); + + (0..8).for_each(|i| { + data.push(Box::new(WordsFilter { word_count: i })).unwrap(); + }); + + // remove removes index-1 from the vec, zero is skipped, and out-of-bounds indices are skipped + data.remove(&mut [0]); + assert_eq!(data.filters.read().unwrap().len(), 8); + + data.remove(&mut [10000]); + assert_eq!(data.filters.read().unwrap().len(), 8); + + // removing 0, 2, 4 + data.remove(&mut [1, 3, 5]); + + assert_eq!(data.filters.read().unwrap().len(), 5); + + let expected = vec![ + WordsFilter { word_count: 1 }, + WordsFilter { word_count: 3 }, + WordsFilter { word_count: 5 }, + WordsFilter { word_count: 6 }, + WordsFilter { word_count: 7 }, + ]; + + for filter in data.filters.read().unwrap().iter() { + let downcast = filter.as_any().downcast_ref::().unwrap(); + assert!(expected.contains(downcast)); + } +} diff --git a/src/filters/utils.rs b/src/filters/utils.rs index 562be72..c291f93 100644 --- a/src/filters/utils.rs +++ b/src/filters/utils.rs @@ -46,6 +46,7 @@ pub(crate) async fn create_similarity_filter( Ok(SimilarityFilter { hash, threshold: SIMILARITY_THRESHOLD, + original_url: similarity_filter.to_string(), }) } @@ -94,13 +95,9 @@ pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option { return Some(Box::new(SimilarityFilter { - // bastardizing the hash field to pass back a url, this means that a caller - // of this function needs to turn the url into a hash. This is a workaround for - // wanting to call this this function from menu.rs but not having access to - // a Handles instance. So, we pass things back up into ferox_scanner.rs and use the - // Handles there to make the actual request. - hash: filter_value.to_string(), + hash: String::new(), threshold: SIMILARITY_THRESHOLD, + original_url: filter_value.to_string(), })); } _ => (), @@ -109,7 +106,6 @@ pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option().unwrap(), &SimilarityFilter { - hash: "http://localhost".to_string(), - threshold: SIMILARITY_THRESHOLD + hash: String::new(), + threshold: SIMILARITY_THRESHOLD, + original_url: "http://localhost".to_string() } ); @@ -199,7 +196,8 @@ mod tests { filter, SimilarityFilter { hash: "3:YKEpn:Yfp".to_string(), - threshold: SIMILARITY_THRESHOLD + threshold: SIMILARITY_THRESHOLD, + original_url: srv.url("/") } ); } diff --git a/src/scan_manager/menu.rs b/src/scan_manager/menu.rs index ae0884a..cbd2e58 100644 --- a/src/scan_manager/menu.rs +++ b/src/scan_manager/menu.rs @@ -9,13 +9,16 @@ use regex::Regex; #[derive(Debug)] pub enum MenuCmd { /// user wants to add a url to be scanned - Add(String), + AddUrl(String), /// user wants to cancel one or more active scans Cancel(Vec, bool), /// user wants to create a new filter - NewFilter(Box), + AddFilter(Box), + + /// user wants to remove one or more active filters + RemoveFilter(Vec), } /// Data container for a command result to be used internally by the ferox_scanner @@ -40,6 +43,9 @@ pub(super) struct Menu { /// footer: instructions surrounded by separators footer: String, + /// unicode line border, matched to longest displayed line + border: String, + /// target for output pub(super) term: Term, } @@ -74,13 +80,13 @@ impl Menu { let new_filter_cmd = format!( " {}[{}] FILTER_TYPE FILTER_VALUE (ex: {} lines 40)\n", - style("n").yellow(), - style("ew-filter").yellow(), - style("n").yellow(), + style("n").green(), + style("ew-filter").green(), + style("n").green(), ); let valid_filters = format!( - " FILTER_TYPEs: {}, {}, {}, {}, {}, {}", + " FILTER_TYPEs: {}, {}, {}, {}, {}, {}\n", style("status").yellow(), style("lines").yellow(), style("size").yellow(), @@ -89,11 +95,20 @@ impl Menu { style("similarity").yellow() ); - let mut commands = String::from("Commands:\n"); + let rm_filter_cmd = format!( + " {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)", + style("r").red(), + style("m-filter").red(), + style("rm-filter").red(), + style("r").red(), + ); + + let mut commands = format!("{}:\n", style("Commands").bright().blue()); commands.push_str(&add_cmd); commands.push_str(&canx_cmd); commands.push_str(&new_filter_cmd); commands.push_str(&valid_filters); + commands.push_str(&rm_filter_cmd); let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name)); @@ -102,11 +117,12 @@ impl Menu { let padded_name = pad_str(&name, longest, Alignment::Center, None); let header = format!("{}\n{}\n{}", border, padded_name, border); - let footer = format!("{}\n{}\n{}", border, commands, border); + let footer = format!("{}\n{}", commands, border); Self { header, footer, + border, term: Term::stderr(), } } @@ -116,6 +132,11 @@ impl Menu { self.println(&self.header); } + /// print menu unicode border line + pub(super) fn print_border(&self) { + self.println(&self.border); + } + /// print menu footer pub(super) fn print_footer(&self) { self.println(&self.footer); @@ -225,7 +246,7 @@ impl Menu { let re = Regex::new(r"^[aA][dD]*").unwrap(); let line = re.replace(line, "").to_string().trim().to_string(); - Some(MenuCmd::Add(line)) + Some(MenuCmd::AddUrl(line)) } 'n' => { // new filter command @@ -238,12 +259,27 @@ impl Menu { // have a string in the filter_value position if let Some(result) = filter_lookup(filter_type, filter_value) { // lookup was successful, return the new filter - return Some(MenuCmd::NewFilter(result)); + return Some(MenuCmd::AddFilter(result)); } } } None } + 'r' => { + // remove filter command + + // remove r[m-filter] from the command so it can be passed to the number + // splitter + let re = Regex::new(r"^[rR][mfilterMFILTER-]*").unwrap(); + // we don't respect a -f or lack thereof in this command, but in case the user + // doesn't realize / thinks its the same as cancel -f, just remove it + let line = line.replace("-f", ""); + let line = re.replace(&line, "").to_string(); + + let indices = self.split_to_nums(&line); + + Some(MenuCmd::RemoveFilter(indices)) + } _ => { // invalid input None diff --git a/src/scan_manager/scan_container.rs b/src/scan_manager/scan_container.rs index f874bfd..cc13a43 100644 --- a/src/scan_manager/scan_container.rs +++ b/src/scan_manager/scan_container.rs @@ -6,7 +6,6 @@ use crate::filters::{ WildcardFilter, WordsFilter, }; use crate::traits::FeroxFilter; -use crate::utils::{create_report_string, ferox_print}; use crate::Command::AddFilter; use crate::{ config::OutputLevel, @@ -15,7 +14,7 @@ use crate::{ scan_manager::{MenuCmd, MenuCmdResult}, scanner::RESPONSES, traits::FeroxSerialize, - SLEEP_DURATION, + Command, SLEEP_DURATION, }; use anyhow::Result; use console::style; @@ -309,6 +308,8 @@ impl FeroxScans { .clone() }; + let mut printed = 0; + for (i, scan) in scans.iter().enumerate() { if matches!(scan.scan_order, ScanOrder::Initial) || scan.task.try_lock().is_err() { // original target passed in via either -u or --stdin @@ -316,12 +317,21 @@ impl FeroxScans { } if matches!(scan.scan_type, ScanType::Directory) { + if printed == 0 { + self.menu + .println(&format!("{}:", style("Scans").bright().blue())); + } // we're only interested in displaying directory scans, as those are // the only ones that make sense to be stopped let scan_msg = format!("{:3}: {}", i, scan); self.menu.println(&scan_msg); + printed += 1; } } + + if printed > 0 { + self.menu.print_border(); + } } /// Given a list of indexes, cancel their associated FeroxScans @@ -372,12 +382,34 @@ impl FeroxScans { num_cancelled } + fn display_filters(&self, handles: Arc) { + let mut printed = 0; + + if let Ok(guard) = handles.filters.data.filters.read() { + for (i, filter) in guard.iter().enumerate() { + if i == 0 { + self.menu + .println(&format!("{}:", style("Filters").bright().blue())); + } + + let filter_msg = format!("{:3}: {}", i + 1, filter); + self.menu.println(&filter_msg); + printed += 1; + } + + if printed > 0 { + self.menu.print_border(); + } + } + } + /// CLI menu that allows for interactive cancellation of recursed-into directories - async fn interactive_menu(&self) -> Option { + async fn interactive_menu(&self, handles: Arc) -> Option { self.menu.hide_progress_bars(); self.menu.clear_screen(); self.menu.print_header(); self.display_scans().await; + self.display_filters(handles.clone()); self.menu.print_footer(); let menu_cmd = if let Ok(line) = self.menu.term.read_line() { @@ -392,11 +424,17 @@ impl FeroxScans { let num_cancelled = self.cancel_scans(indices, should_force).await; Some(MenuCmdResult::NumCancelled(num_cancelled)) } - Some(MenuCmd::Add(url)) => Some(MenuCmdResult::Url(url)), - Some(MenuCmd::NewFilter(filter)) => Some(MenuCmdResult::Filter(filter)), + Some(MenuCmd::AddUrl(url)) => Some(MenuCmdResult::Url(url)), + Some(MenuCmd::AddFilter(filter)) => Some(MenuCmdResult::Filter(filter)), + Some(MenuCmd::RemoveFilter(indices)) => { + handles + .filters + .send(Command::RemoveFilters(indices)) + .unwrap_or_default(); + None + } None => None, }; - log::error!("{}", format!("{:?}", result)); // todo remove self.menu.clear_screen(); self.menu.show_progress_bars(); @@ -449,7 +487,11 @@ impl FeroxScans { /// /// 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) -> Option { + pub async fn pause( + &self, + get_user_input: bool, + handles: Arc, + ) -> Option { // function uses tokio::time, not std // local testing showed a pretty slow increase (less than linear) in CPU usage as # of @@ -461,30 +503,9 @@ impl FeroxScans { INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed); if get_user_input { - command_result = self.interactive_menu().await; + command_result = self.interactive_menu(handles).await; PAUSE_SCAN.store(false, Ordering::Relaxed); self.print_known_responses(); - - if let Some(MenuCmdResult::Filter(_)) = command_result { - // adding/cancelling a url has immediate visual cues, whereas adding a filter - // does not. in order to print this message below already found urls, we need - // to postpone printing until after the call to `self.print_known_responses` - let colored = format!( - "New {} successfully {}!", - style("filter").bright().blue(), - style("created").green() - ); - let msg = create_report_string( - &style("MSG").bright().blue().to_string(), - "-", - "-", - "-", - "-", - &colored, - OutputLevel::Default, - ); - ferox_print(&msg, &PROGRESS_PRINTER); - } } } diff --git a/src/scan_manager/tests.rs b/src/scan_manager/tests.rs index 6ba47fb..0fd6e5e 100644 --- a/src/scan_manager/tests.rs +++ b/src/scan_manager/tests.rs @@ -36,6 +36,7 @@ fn default_scantype_is_file() { async fn scanner_pause_scan_with_finished_spinner() { let now = time::Instant::now(); let urls = FeroxScans::default(); + let handles = Arc::new(Handles::for_testing(None, None).0); PAUSE_SCAN.store(true, Ordering::Relaxed); @@ -46,7 +47,7 @@ async fn scanner_pause_scan_with_finished_spinner() { PAUSE_SCAN.store(false, Ordering::Relaxed); }); - urls.pause(false).await; + urls.pause(false, handles).await; assert!(now.elapsed() > expected); } @@ -400,6 +401,7 @@ fn feroxstates_feroxserialize_implementation() { .push(Box::new(SimilarityFilter { hash: "3:YKEpn:Yfp".to_string(), threshold: SIMILARITY_THRESHOLD, + original_url: "http://localhost:12345/".to_string(), })) .unwrap(); @@ -496,7 +498,7 @@ fn feroxstates_feroxserialize_implementation() { r#""collect_extensions":true"#, r#""collect_backups":false"#, r#""collect_words":false"#, - r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":"3:YKEpn:Yfp","threshold":95}]"#, + r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":"3:YKEpn:Yfp","threshold":95,"original_url":"http://localhost:12345/"}]"#, r#""collected_extensions":["php"]"#, r#""dont_collect":["tif","tiff","ico","cur","bmp","webp","svg","png","jpg","jpeg","jfif","gif","avif","apng","pjpeg","pjp","mov","wav","mpg","mpeg","mp3","mp4","m4a","m4p","m4v","ogg","webm","ogv","oga","flac","aac","3gp","css","zip","xls","xml","gz","tgz"]"#, ] @@ -627,7 +629,7 @@ async fn ferox_scan_abort() { /// and their correctness can be verified easily manually; just calling for now fn menu_print_header_and_footer() { let menu = Menu::new(); - let menu_cmd_1 = MenuCmd::Add(String::from("http://localhost")); + let menu_cmd_1 = MenuCmd::AddUrl(String::from("http://localhost")); let menu_cmd_2 = MenuCmd::Cancel(vec![0], false); let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost")); let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2); @@ -682,9 +684,9 @@ fn menu_get_command_input_from_user_returns_add() { if cmd != "None" { let result = menu.get_command_input_from_user(&full_cmd).unwrap(); - assert!(matches!(result, MenuCmd::Add(_))); + assert!(matches!(result, MenuCmd::AddUrl(_))); - if let MenuCmd::Add(url) = result { + if let MenuCmd::AddUrl(url) = result { assert_eq!(url, test_url); } } else { diff --git a/src/scanner/ferox_scanner.rs b/src/scanner/ferox_scanner.rs index 02925d0..3032ab8 100644 --- a/src/scanner/ferox_scanner.rs +++ b/src/scanner/ferox_scanner.rs @@ -8,7 +8,7 @@ use indicatif::ProgressBar; use lazy_static::lazy_static; use tokio::sync::Semaphore; -use crate::filters::{create_similarity_filter, SimilarityFilter}; +use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter}; use crate::Command::AddFilter; use crate::{ event_handlers::{ @@ -49,7 +49,7 @@ async fn check_for_user_input( // todo write a test or two for this function at some point... if pause_flag.load(Ordering::Acquire) { - match scanned_urls.pause(true).await { + match scanned_urls.pause(true, handles.clone()).await { Some(MenuCmdResult::Url(url)) => { // user wants to add a new url to be scanned, need to send // it over to the event handler for processing @@ -66,10 +66,10 @@ async fn check_for_user_input( } } Some(MenuCmdResult::Filter(mut filter)) => { - let url = if let Some(SimilarityFilter { hash, threshold: _ }) = + let url = if let Some(SimilarityFilter { original_url, .. }) = filter.as_any().downcast_ref::() { - hash.to_owned() + original_url.to_owned() } else { String::new() }; @@ -84,7 +84,12 @@ async fn check_for_user_input( .await .unwrap_or_default(); - filter = Box::new(real_filter) + if real_filter.original_url.is_empty() { + // failed to create filter + filter = Box::new(EmptyFilter {}); + } else { + filter = Box::new(real_filter) + } } handles diff --git a/src/traits.rs b/src/traits.rs index 4ce2a4f..5543959 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,9 +1,14 @@ //! collection of all traits used +use crate::filters::{ + LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter, + WordsFilter, +}; use crate::response::FeroxResponse; use anyhow::Result; +use crossterm::style::{style, Stylize}; use serde::Serialize; use std::any::Any; -use std::fmt::Debug; +use std::fmt::{self, Debug, Display, Formatter}; // references: // https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5 @@ -22,6 +27,36 @@ pub trait FeroxFilter: Debug + Send + Sync { fn as_any(&self) -> &dyn Any; } +impl Display for dyn FeroxFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "Line count: {}", style(filter.line_count).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "Word count: {}", style(filter.word_count).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "Response size: {}", style(filter.content_length).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "Regex: {}", style(&filter.raw_string).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + if filter.dynamic != u64::MAX { + write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan()) + } else { + write!(f, "Static wildcard: {}", style(filter.size).cyan()) + } + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "Status code: {}", style(filter.filter_code).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!( + f, + "Pages similar to: {}", + style(&filter.original_url).cyan() + ) + } else { + write!(f, "Filter: {:?}", self) + } + } +} + /// implementation of PartialEq, necessary long-form due to "trait cannot be made into an object" /// error when attempting to derive PartialEq on the trait itself impl PartialEq for Box {