repo_description: A fast, simple, recursive content discovery tool written in Rust.
repo_topics: content-discovery enumeration hacktoberfest pentest pentesting-tool rust url-bruteforcer web
code: use std::sync::Arc;
  use std::time::Duration;
  
  use crate::filters::filter_lookup;
  use crate::progress::PROGRESS_BAR;
  use crate::sync::DynamicSemaphore;
  use crate::traits::FeroxFilter;
  use console::{measure_text_width, pad_str, style, Alignment, Term};
  use indicatif::{HumanCount, HumanDuration, ProgressDrawTarget};
  use regex::Regex;
  
  /// Data container for a command entered by the user interactively
  #[derive(Debug)]
  pub enum MenuCmd {
      /// user wants to add a url to be scanned
      AddUrl(String),
  
      /// user wants to cancel one or more active scans
      Cancel(Vec<usize>, bool),
  
      /// user wants to create a new filter
      AddFilter(Box<dyn FeroxFilter>),
  
      /// user wants to remove one or more active filters
      RemoveFilter(Vec<usize>),
  
      /// user wants to set the number of scan permits
      SetScanPermits(usize),
  }
  
  /// Data container for a command result to be used internally by the ferox_scanner
  #[derive(Debug)]
  pub enum MenuCmdResult {
      /// Url to be added to the scan queue
      Url(String),
  
      /// Number of scans that were actually cancelled, can be 0
      NumCancelled(usize),
  
      /// Filter to be added to current list of `FeroxFilters`
      Filter(Box<dyn FeroxFilter>),
  
      /// number of permits to be added to the semaphore
      NumPermitsToAdd(usize),
  
      /// number of permits to be subtracted from the semaphore
      NumPermitsToSubtract(usize),
  }
  
  /// Interactive scan cancellation menu
  #[derive(Debug)]
  pub(super) struct Menu {
      /// header: name surrounded by separators
      header: String,
  
      /// footer: instructions surrounded by separators
      footer: String,
  
      /// length of longest displayed line (suitable for ascii/unicode)
      longest: usize,
  
      /// unicode line border, matched to longest displayed line
      border: String,
  
      /// target for output
      pub(super) term: Term,
  }
  
  /// Implementation of Menu
  impl Menu {
      /// Creates new Menu
      pub(super) fn new() -> Self {
          let separator = "─".to_string();
  
          let name = format!(
              "{} {} {}",
              "💀",
              style("Scan Management Menu").bright().yellow(),
              "💀"
          );
  
          let add_cmd = format!(
              "  {}[{}] NEW_URL (ex: {} http://localhost)\n",
              style("a").green(),
              style("dd").green(),
              style("add").green()
          );
  
          let canx_cmd = format!(
              "  {}[{}] [-f] SCAN_ID[-SCAN_ID[,...]] (ex: {} 1-4,8,9-13 or {} -f 3)\n",
              style("c").red(),
              style("ancel").red(),
              style("cancel").red(),
              style("c").red(),
          );
  
          let new_filter_cmd = format!(
              "  {}[{}] FILTER_TYPE FILTER_VALUE (ex: {} lines 40)\n",
              style("n").green(),
              style("ew-filter").green(),
              style("n").green(),
          );
  
          let valid_filters = format!(
              "    FILTER_TYPEs: {}, {}, {}, {}, {}, {}\n",
              style("status").yellow(),
              style("lines").yellow(),
              style("size").yellow(),
              style("words").yellow(),
              style("regex").yellow(),
              style("similarity").yellow()
          );
  
          let rm_filter_cmd = format!(
              "  {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)\n",
              style("r").red(),
              style("m-filter").red(),
              style("rm-filter").red(),
              style("r").red(),
          );
  
          let set_limit_cmd = format!(
              "  {}[{}] VALUE (ex: {} 5)",
              style("s").green(),
              style("et-limit").green(),
              style("set-limit").green(),
          );
  
          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);
          commands.push_str(&set_limit_cmd);
  
          let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name)) + 1;
  
          let border = separator.repeat(longest);
  
          let padded_name = pad_str(&name, longest, Alignment::Center, None);
  
          let header = format!("{border}\n{padded_name}\n{border}");
          let footer = format!("{commands}\n{border}");
  
          Self {
              header,
              footer,
              border,
              longest,
              term: Term::stderr(),
          }
      }
  
      /// print menu header
      pub(super) fn print_header(&self) {
          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);
      }
  
      /// print time remaining in a human-readable format
      pub(super) fn print_eta(&self, eta: Duration) {
          let inner = format!("⏳ {} remaining ⏳", HumanDuration(eta));
          let padded_eta = pad_str(&inner, self.longest, Alignment::Center, None);
          self.println(&format!("{padded_eta}\n{}", self.border));
      }
  
      /// print time remaining in a human-readable format
      pub(super) fn print_scan_limit(&self, limiter: Arc<DynamicSemaphore>) {
          let inner = format!(
              "🦥 Scan limit {}; active {} 🦥",
              HumanCount(limiter.current_capacity() as u64),
              HumanCount(limiter.permits_in_use() as u64)
          );
          let padded_eta = pad_str(&inner, self.longest, Alignment::Center, None);
          self.println(&format!("{padded_eta}\n{}", self.border));
      }
  
      /// set PROGRESS_BAR bar target to hidden
      pub(super) fn hide_progress_bars(&self) {
          PROGRESS_BAR.set_draw_target(ProgressDrawTarget::hidden());
      }
  
      /// set PROGRESS_BAR bar target to hidden
      pub(super) fn show_progress_bars(&self) {
          PROGRESS_BAR.set_draw_target(ProgressDrawTarget::stdout());
      }
  
      /// Wrapper around console's Term::clear_screen and flush
      pub(super) fn clear_screen(&self) {
          self.term.clear_screen().unwrap_or_default();
          self.term.flush().unwrap_or_default();
      }
  
      /// Wrapper around console's Term::write_line
      pub(super) fn println(&self, msg: &str) {
          self.term.write_line(msg).unwrap_or_default();
      }
  
      /// Helper for parsing a usize from a str
      fn str_to_usize(&self, value: &str) -> usize {
          if value.is_empty() {
              return 0;
          }
  
          value
              .trim()
              .to_string()
              .parse::<usize>()
              .unwrap_or_else(|e| {
                  self.println(&format!("Found non-numeric input: {e}: {value:?}"));
                  0
              })
      }
  
      /// split a comma delimited string into vec of usizes
      pub(super) fn split_to_nums(&self, line: &str) -> Vec<usize> {
          let mut nums = Vec::new();
          let values = line.split(',');
  
          for mut value in values {
              value = value.trim();
  
              if value.contains('-') {
                  // range of two values, needs further processing
  
                  let range: Vec<usize> = value
                      .split('-')
                      .map(|s| self.str_to_usize(s))
                      .filter(|m| *m != 0)
                      .collect();
  
                  if range.len() != 2 {
                      // expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1
                      self.println(&format!("Found invalid range of scans: {value}"));
                      continue;
                  }
  
                  (range[0]..=range[1]).for_each(|n| {
                      // iterate from lower to upper bound and add all interim values, skipping
                      // any already known
                      if !nums.contains(&n) {
                          nums.push(n)
                      }
                  });
              } else {
                  if value.is_empty() {
                      continue;
                  }
  
                  let value = self.str_to_usize(value);
  
                  if !nums.contains(&value) {
                      // skip already known values
                      nums.push(value);
                  }
              }
          }
  
          nums
      }
  
      /// get input from the user and translate it to a `MenuCmd`
      pub(super) fn get_command_input_from_user(&self, line: &str) -> Option<MenuCmd> {
          let line = line.trim(); // normalize input if there are leading spaces
  
          match line.chars().next().unwrap_or('_').to_ascii_lowercase() {
              'c' => {
                  // cancel command; start by determining if -f was used
                  let force = line.contains("-f");
  
                  // then remove c[ancel] from the command so it can be passed to the number
                  // splitter
                  let re = Regex::new(r"^[cC][ancelANCEL]*").unwrap();
                  let line = line.replace("-f", "");
                  let line = re.replace(&line, "").to_string();
  
                  Some(MenuCmd::Cancel(self.split_to_nums(&line), force))
              }
              'a' => {
                  // add command
                  // similar to cancel, we need to remove the a[dd] substring, the rest should be
                  // a url
                  let re = Regex::new(r"^[aA][dD]*").unwrap();
                  let line = re.replace(line, "").to_string().trim().to_string();
  
                  Some(MenuCmd::AddUrl(line))
              }
              'n' => {
                  // new filter command
                  let mut line = line.split_whitespace();
                  line.next(); // 'n' or 'new-filter'
  
                  if let Some(filter_type) = line.next() {
                      // have a string in the filter_type position
                      if let Some(filter_value) = line.next() {
                          // 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::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))
              }
              's' => {
                  // set scan permits
  
                  // remove s[et-limit] from the command so it can be passed to the number
                  // splitter
                  let re = Regex::new(r"^[sS][etETlimitLIMIT-]*").unwrap();
                  let line = re.replace(line, "").trim().to_string();
  
                  let Ok(value) = line.parse::<usize>() else {
                      return None;
                  };
  
                  if value == 0 {
                      // if the value is 0, we don't want to set the limit, so return None
                      return None;
                  }
  
                  Some(MenuCmd::SetScanPermits(value))
              }
              _ => {
                  // invalid input
                  None
              }
          }
      }
  
      /// Given a url, confirm with user that we should cancel
      pub(super) fn confirm_cancellation(&self, url: &str) -> char {
          self.println(&format!(
              "You sure you wanna cancel this scan: {url}? [Y/n]"
          ));
  
          self.term.read_char().unwrap_or('n')
      }
  }
  
  /// Default implementation for Menu
  impl Default for Menu {
      /// return Menu::new as default
      fn default() -> Menu {
          Menu::new()
      }
  }
  