From a1728104ddf31003877760b0fb3133018e455b69 Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 10:31:25 +0000 Subject: [PATCH 1/7] Add UI tests for argument parsing --- Cargo.lock | 28 +- Cargo.toml | 4 + src/arg_parse.rs | 1097 +++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 51 +-- 4 files changed, 1115 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71c1bef..f4ad1c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,6 +314,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dirble" version = "1.4.2" @@ -327,6 +333,7 @@ dependencies = [ "encoding", "log", "percent-encoding", + "pretty_assertions", "rand 0.9.0", "select", "serde", @@ -335,6 +342,7 @@ dependencies = [ "simple_xml_serialize", "simple_xml_serialize_macro", "simplelog", + "tempfile", "time", "url", "vergen-gix", @@ -1640,6 +1648,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -2090,9 +2108,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.1", @@ -2560,6 +2578,12 @@ dependencies = [ "markup5ever", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 52deb2d..ebb9f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,7 @@ vergen-gix = { version = "1.0.6", features = ["build", "si"] } [features] release_version_string = [] + +[dev-dependencies] +pretty_assertions = "1.4.1" +tempfile = "3.19.1" diff --git a/src/arg_parse.rs b/src/arg_parse.rs index 2a02b80..05d1dc1 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -17,12 +17,12 @@ use crate::wordlist::lines_from_file; use atty::Stream; -use clap::{App, AppSettings, Arg, ArgGroup, arg_enum, crate_version, value_t}; +use clap::{arg_enum, crate_version, value_t, App, AppSettings, Arg, ArgGroup}; use simplelog::LevelFilter; -use std::{fmt, process::exit}; +use std::{ffi::OsString, fmt, process::exit}; use url::Url; -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct GlobalOpts { pub hostnames: Vec, pub wordlist_files: Option>, @@ -32,7 +32,7 @@ pub struct GlobalOpts { pub max_threads: u32, pub proxy_enabled: bool, pub proxy_address: String, - #[expect(dead_code, reason = "TODO")] + #[allow(dead_code, reason = "TODO")] pub proxy_auth_enabled: bool, pub ignore_cert: bool, pub show_htaccess: bool, @@ -62,7 +62,7 @@ pub struct GlobalOpts { pub length_blacklist: LengthRanges, } -#[derive(PartialOrd, Ord, PartialEq, Eq, Clone)] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] pub struct LengthRange { pub start: usize, pub end: Option, @@ -90,7 +90,7 @@ impl fmt::Debug for LengthRange { } } -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct LengthRanges { pub ranges: Vec, } @@ -118,14 +118,14 @@ impl fmt::Display for LengthRanges { } } -#[derive(Clone, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct ScanOpts { pub scan_401: bool, pub scan_403: bool, } arg_enum! { - #[derive(Clone)] + #[derive(Copy,Clone, Debug,PartialEq,Eq)] pub enum HttpVerb { Get, Head, @@ -148,7 +148,11 @@ enum FileTypes { } #[allow(clippy::cognitive_complexity)] -pub fn get_args() -> GlobalOpts { +pub fn get_args(args: ArgsIter) -> GlobalOpts +where + ArgsIter: IntoIterator, + ArgsIter::Item: Into + Clone, +{ // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. @@ -560,7 +564,7 @@ set to 0 to disable") .next_line_help(true) .takes_value(true) .value_delimiter(",")) - .get_matches(); + .get_matches_from(args); let mut hostnames: Vec = Vec::new(); @@ -957,10 +961,58 @@ fn length_blacklist_parse(blacklist_inputs: clap::Values) -> LengthRanges { #[cfg(test)] mod test { + use super::*; + use log::LevelFilter::Info; + use pretty_assertions::assert_eq; + use std::io::Write; + use tempfile::NamedTempFile; + + impl Default for GlobalOpts { + fn default() -> Self { + GlobalOpts { + hostnames: Default::default(), + wordlist_files: Default::default(), + prefixes: vec!["".into()], + extensions: vec!["".into()], + extension_substitution: false, + max_threads: 10, + proxy_enabled: Default::default(), + proxy_address: Default::default(), + proxy_auth_enabled: Default::default(), + ignore_cert: Default::default(), + show_htaccess: Default::default(), + throttle: Default::default(), + max_recursion_depth: Default::default(), + user_agent: Default::default(), + username: Default::default(), + password: Default::default(), + output_file: Default::default(), + json_file: Default::default(), + xml_file: Default::default(), + timeout: 5, + max_errors: 5, + wordlist_split: 3, + scan_listable: Default::default(), + cookies: Default::default(), + headers: Default::default(), + scrape_listable: Default::default(), + whitelist: Default::default(), + code_list: Default::default(), + is_terminal: Default::default(), + no_color: Default::default(), + disable_validator: Default::default(), + http_verb: Default::default(), + scan_opts: Default::default(), + log_level: Info, + length_blacklist: Default::default(), + } + } + } + #[test] fn argparse_length_range_contains() { // Range with start and end values - let range = crate::arg_parse::LengthRange { + let range = LengthRange { start: 3, end: Some(6), }; @@ -976,7 +1028,7 @@ mod test { assert!(!range.contains(7)); // Range with just a start value - let range = crate::arg_parse::LengthRange { + let range = LengthRange { start: 5, end: None, }; @@ -991,17 +1043,17 @@ mod test { #[test] fn argparse_length_ranges_contain() { // Empty range - let ranges: crate::arg_parse::LengthRanges = Default::default(); + let ranges: LengthRanges = Default::default(); assert!(!ranges.contains(4)); // Non-overlapping ranges - let ranges = crate::arg_parse::LengthRanges { + let ranges = LengthRanges { ranges: vec![ - crate::arg_parse::LengthRange { + LengthRange { start: 4, end: Some(10), }, - crate::arg_parse::LengthRange { + LengthRange { start: 15, end: Some(18), }, @@ -1018,4 +1070,1017 @@ mod test { // too large assert!(!ranges.contains(19)); } + + #[test] + fn test_int_checks() { + let expected_err = "The number given must be a positive integer."; + positive_int_check("1".into()).unwrap(); + positive_int_check(u32::MAX.to_string()).unwrap(); + assert_eq!(positive_int_check("0".into()).unwrap_err(), expected_err); + assert_eq!(positive_int_check("-1".into()).unwrap_err(), expected_err); + assert_eq!( + positive_int_check((u32::MAX as u64 + 1).to_string()).unwrap_err(), + expected_err + ); + assert_eq!( + positive_int_check("text".into()).unwrap_err(), + expected_err + ); + assert_eq!(positive_int_check("".into()).unwrap_err(), expected_err); + + let expected_err = "The number given must be an integer."; + int_check("1".into()).unwrap(); + int_check("0".into()).unwrap(); + int_check(u32::MAX.to_string()).unwrap(); + assert_eq!(int_check("-1".into()).unwrap_err(), expected_err); + assert_eq!( + int_check((u32::MAX as u64 + 1).to_string()).unwrap_err(), + expected_err + ); + assert_eq!(int_check("text".into()).unwrap_err(), expected_err); + assert_eq!(int_check("".into()).unwrap_err(), expected_err); + } + + #[track_caller] + fn assert_args(args: ArgsIter, expected: GlobalOpts) + where + ArgsIter: IntoIterator, + ArgsIter::Item: Into + Clone, + { + let mut opts = get_args(args); + // normalise the is-terminal value as it's not useful to test + opts.is_terminal = expected.is_terminal; + let info = std::panic::Location::caller(); + assert_eq!(opts, expected, "Caller: {}:{}", info.file(), info.line()); + } + + fn make_uri_file() -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + write!( + &mut file, + "http://some-host + https://other-host" + ) + .unwrap(); + file + } + + fn make_extensions_file() -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + writeln!(&mut file, "txt").unwrap(); + writeln!(&mut file, "jpg").unwrap(); + writeln!(&mut file, "png").unwrap(); + file + } + + #[test] + fn required_args() { + assert_args( + ["test", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ..Default::default() + }, + ); + assert_args( + ["test", "-u", "http://some-host", "-u", "https://other-host"], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + let uri_file = make_uri_file(); + assert_args( + ["test", "-U", &uri_file.path().display().to_string()], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "-U", + &uri_file.path().display().to_string(), + "-u", + "http://third-host", + ], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "http://third-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + } + + #[test] + fn http_verb() { + assert_args( + ["test", "--verb", "Get", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ..Default::default() + }, + ); + assert_args( + ["test", "--verb", "Post", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Post, + ..Default::default() + }, + ); + assert_args( + ["test", "--verb", "Head", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Head, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--verb", "Head"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Head, + ..Default::default() + }, + ); + } + + #[test] + fn wordlist() { + assert_args( + ["test", "http://some-host", "--wordlist", "file-one"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_files: Some(vec!["file-one".into()]), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--wordlist", + "file-one", + "-w", + "file-two", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_files: Some(vec![ + "file-one".into(), + "file-two".into(), + ]), + ..Default::default() + }, + ); + } + + #[test] + fn extensions() { + assert_args( + ["test", "http://some-host", "--extensions", "txt"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: vec![String::new(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--extensions", "txt,jpg"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: vec![String::new(), "jpg".into(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--extensions", + "txt,jpg", + "-x", + "png", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn extension_file() { + let exts_file = make_extensions_file(); + assert_args( + [ + "test", + "http://some-host", + "--extension-file", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-X", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-X", + &exts_file.path().display().to_string(), + "-x", + "rs", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn ext_sub() { + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + "--force-extension", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + "-f", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + } + + #[test] + fn prefixes() { + assert_args( + ["test", "http://some-host", "--prefixes", "txt"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: vec![String::new(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--prefixes", "txt,jpg"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: vec![String::new(), "jpg".into(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--prefixes", + "txt,jpg", + "-p", + "png", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn prefix_file() { + let exts_file = make_extensions_file(); + assert_args( + [ + "test", + "http://some-host", + "--prefix-file", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-P", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-P", + &exts_file.path().display().to_string(), + "-p", + "rs", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn output_file() { + assert_args( + ["test", "http://some-host", "-o", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oN", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn json_file() { + assert_args( + ["test", "http://some-host", "--json-file", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + json_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oJ", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + json_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn xml_file() { + assert_args( + ["test", "http://some-host", "--xml-file", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + xml_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oX", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + xml_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn output_all() { + assert_args( + ["test", "http://some-host", "--output-all", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file.txt".into()), + json_file: Some("some-file.json".into()), + xml_file: Some("some-file.xml".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oA", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file.txt".into()), + json_file: Some("some-file.json".into()), + xml_file: Some("some-file.xml".into()), + ..Default::default() + }, + ); + } + + #[test] + fn proxy() { + assert_args( + ["test", "http://some-host", "--proxy", "http://proxy:8000"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + proxy_enabled: true, + proxy_address: "http://proxy:8000".into(), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--burp"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + proxy_enabled: true, + proxy_address: "http://localhost:8080".into(), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--no-proxy"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + //TODO should this set proxy_enabled to false? + proxy_enabled: true, + proxy_address: String::new(), + ..Default::default() + }, + ); + } + + #[test] + fn max_threads() { + assert_args( + ["test", "http://some-host", "--max-threads", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_threads: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-t", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_threads: 4, + ..Default::default() + }, + ); + } + + #[test] + fn wordlist_split() { + assert_args( + ["test", "http://some-host", "--wordlist-split", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_split: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-T", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_split: 4, + ..Default::default() + }, + ); + } + + #[test] + fn throttle() { + assert_args( + ["test", "http://some-host", "--throttle", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + throttle: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-z", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + throttle: 4, + ..Default::default() + }, + ); + } + + #[test] + fn username_password() { + assert_args( + [ + "test", + "http://some-host", + "--username", + "user", + "--password", + "pass", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + username: Some("user".into()), + password: Some("pass".into()), + ..Default::default() + }, + ); + } + + #[test] + fn recursion() { + assert_args( + ["test", "http://some-host", "--disable-recursion"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(0), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--max-recursion-depth", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(4), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--max-recursion-depth", + "4", + "--disable-recursion", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(0), + ..Default::default() + }, + ); + } + + #[test] + fn listable() { + assert_args( + ["test", "http://some-host", "--scan-listable"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_listable: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--scrape-listable"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scrape_listable: true, + ..Default::default() + }, + ); + } + + #[test] + fn cookie() { + assert_args( + [ + "test", + "http://some-host", + "--cookie", + "name1=value1", + "-c", + "name2=value2", + "-c", + "name3=value3", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + cookies: Some( + "name1=value1; name2=value2; name3=value3".into(), + ), + ..Default::default() + }, + ); + } + + #[test] + fn header() { + assert_args( + [ + "test", + "http://some-host", + "--header", + "header1:value1", + "-H", + "header2: value2", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + headers: Some( + ["header1:value1", "header2: value2"] + .into_iter() + .map(Into::into) + .collect(), + ), + ..Default::default() + }, + ); + } + + #[test] + fn user_agent() { + assert_args( + [ + "test", + "http://some-host", + "--user-agent", + "some user agent", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + user_agent: Some("some user agent".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-a", "some user agent"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + user_agent: Some("some user agent".into()), + ..Default::default() + }, + ); + } + + #[test] + fn verbosity() { + assert_args( + ["test", "http://some-host", "--verbose"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Debug, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--verbose", "--verbose"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--verbose", + "--verbose", + "--verbose", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-v"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Debug, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-vv"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-vvv"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + } + + #[test] + fn silent() { + assert_args( + ["test", "http://some-host", "--silent"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Warn, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-S"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Warn, + ..Default::default() + }, + ); + } + + #[test] + fn code_whitelist() { + assert_args( + [ + "test", + "http://some-host", + "--code-whitelist", + "200,201,204", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: true, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-W", "200,201,204"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: true, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + } + + #[test] + fn code_blacklist() { + assert_args( + [ + "test", + "http://some-host", + "--code-blacklist", + "200,201,204", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: false, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-B", "200,201,204"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: false, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + } + + #[test] + fn disable_validator() { + assert_args( + ["test", "http://some-host", "--disable-validator"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + disable_validator: true, + ..Default::default() + }, + ); + } + + #[test] + fn scan_opts() { + assert_args( + ["test", "http://some-host", "--scan-401"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_opts: ScanOpts { + scan_401: true, + scan_403: false, + }, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--scan-403"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_opts: ScanOpts { + scan_401: false, + scan_403: true, + }, + ..Default::default() + }, + ); + } + + #[test] + fn ignore_cert() { + assert_args( + ["test", "http://some-host", "--ignore-cert"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ignore_cert: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-k"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ignore_cert: true, + ..Default::default() + }, + ); + } + + #[test] + fn show_htaccess() { + assert_args( + ["test", "http://some-host", "--show-htaccess"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + show_htaccess: true, + ..Default::default() + }, + ); + } + + #[test] + fn timeout() { + assert_args( + ["test", "http://some-host", "--timeout", "2"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + timeout: 2, + ..Default::default() + }, + ); + } + + #[test] + fn max_errors() { + assert_args( + ["test", "http://some-host", "--max-errors", "2"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_errors: 2, + ..Default::default() + }, + ); + } + + #[test] + fn no_colour() { + assert_args( + ["test", "http://some-host", "--no-colour"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + no_color: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--no-color"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + no_color: true, + ..Default::default() + }, + ); + } + + #[test] + fn hide_lengths() { + assert_args( + ["test", "http://some-host", "--hide-lengths", "400,600-700"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + length_blacklist: LengthRanges { + ranges: vec![ + LengthRange { + start: 400, + end: None, + }, + LengthRange { + start: 600, + end: Some(700), + }, + ], + }, + ..Default::default() + }, + ); + } } diff --git a/src/main.rs b/src/main.rs index fb82b47..bb579b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,16 +15,16 @@ // You should have received a copy of the GNU General Public License // along with Dirble. If not, see . -use log::{LevelFilter, debug, error, info, warn}; +use log::{debug, error, info, warn, LevelFilter}; use simplelog::{ColorChoice, TermLogger, TerminalMode}; use std::{ collections::VecDeque, env::current_exe, path::Path, sync::{ - Arc, atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, Sender}, + Arc, }, thread, time::Duration, @@ -44,7 +44,7 @@ mod wordlist; #[allow(clippy::cognitive_complexity)] fn main() { // Read the arguments in using the arg_parse module - let global_opts = Arc::new(arg_parse::get_args()); + let global_opts = Arc::new(arg_parse::get_args(std::env::args_os())); // Prepare the logging handler. Default to a pretty TermLogger, // but if the TermLogger initialisation fails (e.g. if we are not @@ -388,52 +388,9 @@ fn generate_end() -> request::RequestResponse { #[cfg(test)] mod test { - use crate::{arg_parse::GlobalOpts, request::RequestResponse}; - use log::LevelFilter::Info; + use crate::request::RequestResponse; use url::Url; - impl Default for GlobalOpts { - fn default() -> Self { - GlobalOpts { - hostnames: Default::default(), - wordlist_files: Default::default(), - prefixes: vec!["".into()], - extensions: vec!["".into()], - extension_substitution: false, - max_threads: Default::default(), - proxy_enabled: Default::default(), - proxy_address: Default::default(), - proxy_auth_enabled: Default::default(), - ignore_cert: Default::default(), - show_htaccess: Default::default(), - throttle: Default::default(), - max_recursion_depth: Default::default(), - user_agent: Default::default(), - username: Default::default(), - password: Default::default(), - output_file: Default::default(), - json_file: Default::default(), - xml_file: Default::default(), - timeout: Default::default(), - max_errors: Default::default(), - wordlist_split: Default::default(), - scan_listable: Default::default(), - cookies: Default::default(), - headers: Default::default(), - scrape_listable: Default::default(), - whitelist: Default::default(), - code_list: Default::default(), - is_terminal: Default::default(), - no_color: Default::default(), - disable_validator: Default::default(), - http_verb: Default::default(), - scan_opts: Default::default(), - log_level: Info, - length_blacklist: Default::default(), - } - } - } - impl Default for RequestResponse { fn default() -> Self { RequestResponse { From dd451dc2786002633b9913b39800b56a461f553f Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 11:02:33 +0000 Subject: [PATCH 2/7] Refactor clap app into function --- src/arg_parse.rs | 435 ++++++++++++++++++++++++----------------------- 1 file changed, 219 insertions(+), 216 deletions(-) diff --git a/src/arg_parse.rs b/src/arg_parse.rs index 05d1dc1..a3781b4 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -153,12 +153,229 @@ where ArgsIter: IntoIterator, ArgsIter::Item: Into + Clone, { + // Defines all the command line arguments with the Clap module + let args = app().get_matches_from(args); + + let mut hostnames: Vec = Vec::new(); + + // Get from host arguments + if let Some(host) = args.value_of("host") { + if let Ok(host) = Url::parse(host) { + hostnames.push(host); + } else { + println!("Invaid URL: {}", host); + } + } + if let Some(host_files) = args.values_of("host_file") { + for host_file in host_files { + let hosts = lines_from_file(&String::from(host_file)); + for hostname in hosts { + if url_is_valid(hostname.clone()).is_ok() { + if let Ok(host) = Url::parse(hostname.as_str()) { + hostnames.push(host); + } + } else { + println!("Invalid URL: {}", hostname); + } + } + } + } + if let Some(extra_hosts) = args.values_of("extra_hosts") { + for hostname in extra_hosts { + if let Ok(host) = Url::parse(hostname) { + hostnames.push(host); + } + } + } + + if hostnames.is_empty() { + println!("No valid hosts were provided - exiting"); + exit(2); + } + hostnames.sort(); + hostnames.dedup(); + + // Parse wordlist file names into a vector + let wordlists: Option> = + if let Some(wordlist_files) = args.values_of("wordlist") { + let mut wordlists_vec = Vec::new(); + for wordlist_file in wordlist_files { + wordlists_vec.push(String::from(wordlist_file)); + } + Some(wordlists_vec) + } else { + None + }; + + // Check for proxy related flags + let proxy_enabled; + let proxy_address; + if let Some(proxy_addr) = args.value_of("proxy") { + proxy_enabled = true; + proxy_address = proxy_addr; + if proxy_address == "http://localhost:8080" { + println!( + "You could use the --burp flag instead of the --proxy flag!" + ); + } + } else if args.is_present("burp") { + proxy_enabled = true; + proxy_address = "http://localhost:8080"; + } else if args.is_present("no_proxy") { + proxy_enabled = true; + proxy_address = ""; + } else { + proxy_enabled = false; + proxy_address = ""; + } + let proxy_address = String::from(proxy_address); + + // Read provided cookie values into a vector + let cookies: Option = + if let Some(cookies) = args.values_of("cookie") { + let mut temp_cookies: Vec = Vec::new(); + for cookie in cookies { + temp_cookies.push(String::from(cookie)); + } + Some(temp_cookies.join("; ")) + } else { + None + }; + + // Read provided headers into a vector + let headers: Option> = + if let Some(headers) = args.values_of("header") { + let mut temp_headers: Vec = Vec::new(); + for header in headers { + temp_headers.push(String::from(header)); + } + Some(temp_headers) + } else { + None + }; + + let mut whitelist = false; + let mut code_list: Vec = Vec::new(); + + if let Some(whitelist_values) = args.values_of("code_whitelist") { + whitelist = true; + for code in whitelist_values { + code_list.push(code.parse::().expect("Code is an integer")); + } + } else if let Some(blacklist_values) = args.values_of("code_blacklist") { + whitelist = false; + for code in blacklist_values { + code_list.push(code.parse::().expect("Code is an integer")); + } + } + + let mut max_recursion_depth = None; + if args.is_present("disable_recursion") { + max_recursion_depth = Some(0); + } else if let Some(depth) = args.value_of("max_recursion_depth") { + max_recursion_depth = + Some(depth.parse::().expect("Recursion depth is an integer")); + } + + let mut scan_opts = ScanOpts { + scan_401: false, + scan_403: false, + }; + if args.is_present("scan_401") || (whitelist && code_list.contains(&401)) { + scan_opts.scan_401 = true; + } + + if args.is_present("scan_403") || (whitelist && code_list.contains(&403)) { + scan_opts.scan_403 = true; + } + + // Configure the logging level. The silent flag overrides any + // verbose flags in use. + let log_level = if args.is_present("silent") { + LevelFilter::Warn + } else { + match args.occurrences_of("verbose") { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + } + }; + + // Create the GlobalOpts struct and return it + GlobalOpts { + hostnames, + wordlist_files: wordlists, + prefixes: load_modifiers(&args, "prefixes"), + extensions: load_modifiers(&args, "extensions"), + extension_substitution: args.is_present("extension_substitution"), + max_threads: args + .value_of("max_threads") + .expect("Max threads is set") + .parse::() + .expect("Max threads is an integer"), + proxy_enabled, + proxy_address, + proxy_auth_enabled: false, + ignore_cert: args.is_present("ignore_cert"), + show_htaccess: args.is_present("show_htaccess"), + throttle: if let Some(throttle) = args.value_of("throttle") { + throttle.parse::().expect("Throttle is an integer") + } else { + 0 + }, + max_recursion_depth, + user_agent: args.value_of("user_agent").map(String::from), + // Dependency between username and password is handled by Clap + username: args.value_of("username").map(String::from), + // Dependency between username and password is handled by Clap + password: args.value_of("password").map(String::from), + output_file: filename_from_args(&args, FileTypes::Txt), + json_file: filename_from_args(&args, FileTypes::Json), + xml_file: filename_from_args(&args, FileTypes::Xml), + timeout: args + .value_of("timeout") + .expect("Timeout is set") + .parse::() + .expect("Timeout is an integer"), + max_errors: args + .value_of("max_errors") + .expect("Max errors is set") + .parse::() + .expect("Max errors is an integer"), + wordlist_split: args + .value_of("wordlist_split") + .expect("Wordlist split is set") + .parse::() + .expect("Wordlist split is an integer"), + scan_listable: args.is_present("scan_listable"), + cookies, + headers, + scrape_listable: args.is_present("scrape_listable"), + whitelist, + code_list, + is_terminal: atty::is(Stream::Stdout), + no_color: args.is_present("no_color"), + disable_validator: args.is_present("disable_validator"), + http_verb: value_t!(args.value_of("http_verb"), HttpVerb) + .expect("Must be valid HTTP verb"), + scan_opts, + log_level, + length_blacklist: if let Some(lengths) = + args.values_of("length_blacklist") + { + length_blacklist_parse(lengths) + } else { + Default::default() + }, + } +} + +fn app() -> App<'static, 'static> { // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. let version_string = get_version_string(); - // Defines all the command line arguments with the Clap module - let args = App::new("Dirble") + App::new("Dirble") .version(version_string) .author( "Developed by Izzy Whistlecroft \ @@ -564,220 +781,6 @@ set to 0 to disable") .next_line_help(true) .takes_value(true) .value_delimiter(",")) - .get_matches_from(args); - - let mut hostnames: Vec = Vec::new(); - - // Get from host arguments - if let Some(host) = args.value_of("host") { - if let Ok(host) = Url::parse(host) { - hostnames.push(host); - } else { - println!("Invaid URL: {}", host); - } - } - if let Some(host_files) = args.values_of("host_file") { - for host_file in host_files { - let hosts = lines_from_file(&String::from(host_file)); - for hostname in hosts { - if url_is_valid(hostname.clone()).is_ok() { - if let Ok(host) = Url::parse(hostname.as_str()) { - hostnames.push(host); - } - } else { - println!("Invalid URL: {}", hostname); - } - } - } - } - if let Some(extra_hosts) = args.values_of("extra_hosts") { - for hostname in extra_hosts { - if let Ok(host) = Url::parse(hostname) { - hostnames.push(host); - } - } - } - - if hostnames.is_empty() { - println!("No valid hosts were provided - exiting"); - exit(2); - } - hostnames.sort(); - hostnames.dedup(); - - // Parse wordlist file names into a vector - let wordlists: Option> = - if let Some(wordlist_files) = args.values_of("wordlist") { - let mut wordlists_vec = Vec::new(); - for wordlist_file in wordlist_files { - wordlists_vec.push(String::from(wordlist_file)); - } - Some(wordlists_vec) - } else { - None - }; - - // Check for proxy related flags - let proxy_enabled; - let proxy_address; - if let Some(proxy_addr) = args.value_of("proxy") { - proxy_enabled = true; - proxy_address = proxy_addr; - if proxy_address == "http://localhost:8080" { - println!( - "You could use the --burp flag instead of the --proxy flag!" - ); - } - } else if args.is_present("burp") { - proxy_enabled = true; - proxy_address = "http://localhost:8080"; - } else if args.is_present("no_proxy") { - proxy_enabled = true; - proxy_address = ""; - } else { - proxy_enabled = false; - proxy_address = ""; - } - let proxy_address = String::from(proxy_address); - - // Read provided cookie values into a vector - let cookies: Option = - if let Some(cookies) = args.values_of("cookie") { - let mut temp_cookies: Vec = Vec::new(); - for cookie in cookies { - temp_cookies.push(String::from(cookie)); - } - Some(temp_cookies.join("; ")) - } else { - None - }; - - // Read provided headers into a vector - let headers: Option> = - if let Some(headers) = args.values_of("header") { - let mut temp_headers: Vec = Vec::new(); - for header in headers { - temp_headers.push(String::from(header)); - } - Some(temp_headers) - } else { - None - }; - - let mut whitelist = false; - let mut code_list: Vec = Vec::new(); - - if let Some(whitelist_values) = args.values_of("code_whitelist") { - whitelist = true; - for code in whitelist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } - } else if let Some(blacklist_values) = args.values_of("code_blacklist") { - whitelist = false; - for code in blacklist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } - } - - let mut max_recursion_depth = None; - if args.is_present("disable_recursion") { - max_recursion_depth = Some(0); - } else if let Some(depth) = args.value_of("max_recursion_depth") { - max_recursion_depth = - Some(depth.parse::().expect("Recursion depth is an integer")); - } - - let mut scan_opts = ScanOpts { - scan_401: false, - scan_403: false, - }; - if args.is_present("scan_401") || (whitelist && code_list.contains(&401)) { - scan_opts.scan_401 = true; - } - - if args.is_present("scan_403") || (whitelist && code_list.contains(&403)) { - scan_opts.scan_403 = true; - } - - // Configure the logging level. The silent flag overrides any - // verbose flags in use. - let log_level = if args.is_present("silent") { - LevelFilter::Warn - } else { - match args.occurrences_of("verbose") { - 0 => LevelFilter::Info, - 1 => LevelFilter::Debug, - _ => LevelFilter::Trace, - } - }; - - // Create the GlobalOpts struct and return it - GlobalOpts { - hostnames, - wordlist_files: wordlists, - prefixes: load_modifiers(&args, "prefixes"), - extensions: load_modifiers(&args, "extensions"), - extension_substitution: args.is_present("extension_substitution"), - max_threads: args - .value_of("max_threads") - .expect("Max threads is set") - .parse::() - .expect("Max threads is an integer"), - proxy_enabled, - proxy_address, - proxy_auth_enabled: false, - ignore_cert: args.is_present("ignore_cert"), - show_htaccess: args.is_present("show_htaccess"), - throttle: if let Some(throttle) = args.value_of("throttle") { - throttle.parse::().expect("Throttle is an integer") - } else { - 0 - }, - max_recursion_depth, - user_agent: args.value_of("user_agent").map(String::from), - // Dependency between username and password is handled by Clap - username: args.value_of("username").map(String::from), - // Dependency between username and password is handled by Clap - password: args.value_of("password").map(String::from), - output_file: filename_from_args(&args, FileTypes::Txt), - json_file: filename_from_args(&args, FileTypes::Json), - xml_file: filename_from_args(&args, FileTypes::Xml), - timeout: args - .value_of("timeout") - .expect("Timeout is set") - .parse::() - .expect("Timeout is an integer"), - max_errors: args - .value_of("max_errors") - .expect("Max errors is set") - .parse::() - .expect("Max errors is an integer"), - wordlist_split: args - .value_of("wordlist_split") - .expect("Wordlist split is set") - .parse::() - .expect("Wordlist split is an integer"), - scan_listable: args.is_present("scan_listable"), - cookies, - headers, - scrape_listable: args.is_present("scrape_listable"), - whitelist, - code_list, - is_terminal: atty::is(Stream::Stdout), - no_color: args.is_present("no_color"), - disable_validator: args.is_present("disable_validator"), - http_verb: value_t!(args.value_of("http_verb"), HttpVerb) - .expect("Must be valid HTTP verb"), - scan_opts, - log_level, - length_blacklist: if let Some(lengths) = - args.values_of("length_blacklist") - { - length_blacklist_parse(lengths) - } else { - Default::default() - }, - } } /// filetype is one of "txt", "json", and "xml". Returns a filename that is From 962767495a005d9f8538adddb4936f9a44802e84 Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 11:25:47 +0000 Subject: [PATCH 3/7] Update clap from 2 to 3 --- Cargo.lock | 80 ++++++++++++++++-------------- Cargo.toml | 2 +- src/arg_parse.rs | 123 ++++++++++++++++++++++++++--------------------- 3 files changed, 112 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4ad1c3..549089c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,15 +26,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.97" @@ -131,17 +122,27 @@ checksum = "1a48563284b67c003ba0fb7243c87fab68885e1532c605704228a80238512e31" [[package]] name = "clap" -version = "2.34.0" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ - "ansi_term", "atty", "bitflags 1.3.2", - "strsim 0.8.0", + "clap_lex", + "indexmap", + "once_cell", + "strsim 0.10.0", + "termcolor", "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", ] [[package]] @@ -764,7 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" dependencies = [ "gix-hash", - "hashbrown", + "hashbrown 0.14.5", "parking_lot", ] @@ -787,7 +788,7 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown", + "hashbrown 0.14.5", "itoa", "libc", "memmap2", @@ -1102,6 +1103,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1289,6 +1296,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1520,6 +1537,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2038,9 +2061,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" @@ -2141,12 +2164,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.11.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" [[package]] name = "thiserror" @@ -2247,12 +2267,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-xid" version = "0.1.0" @@ -2294,12 +2308,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "vergen" version = "9.0.4" diff --git a/Cargo.toml b/Cargo.toml index ebb9f83..2143d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ description = "Fast directory scanning and scraping tool" [dependencies] curl = "0.4.19" percent-encoding = "2.1" -clap = "2.32" +clap = { version = "3.2.25", features = ["cargo"] } select = "0.6" chardet = "0.2.4" encoding = "0.2.33" diff --git a/src/arg_parse.rs b/src/arg_parse.rs index a3781b4..abe927d 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -17,7 +17,10 @@ use crate::wordlist::lines_from_file; use atty::Stream; -use clap::{arg_enum, crate_version, value_t, App, AppSettings, Arg, ArgGroup}; +use clap::{ + arg_enum, crate_version, value_t, App, AppSettings, Arg, ArgAction, + ArgGroup, +}; use simplelog::LevelFilter; use std::{ffi::OsString, fmt, process::exit}; use url::Url; @@ -170,7 +173,7 @@ where for host_file in host_files { let hosts = lines_from_file(&String::from(host_file)); for hostname in hosts { - if url_is_valid(hostname.clone()).is_ok() { + if url_is_valid(&hostname).is_ok() { if let Ok(host) = Url::parse(hostname.as_str()) { hostnames.push(host); } @@ -294,7 +297,7 @@ where let log_level = if args.is_present("silent") { LevelFilter::Warn } else { - match args.occurrences_of("verbose") { + match args.get_count("verbose") { 0 => LevelFilter::Info, 1 => LevelFilter::Debug, _ => LevelFilter::Trace, @@ -370,7 +373,7 @@ where } } -fn app() -> App<'static, 'static> { +fn app() -> App<'static> { // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. @@ -416,7 +419,7 @@ http://user:pass@host:port") .long("uri") .multiple(true) .next_line_help(true) - .short("u") + .short('u') .takes_value(true) .validator(url_is_valid) .value_name("uri") @@ -430,7 +433,7 @@ headers set will be applied to all URIs") .long("uri-file") .multiple(true) .next_line_help(true) - .short("U") + .short('U') .takes_value(true) .value_name("uri-file") .visible_alias("url-file")) @@ -446,7 +449,7 @@ headers set will be applied to all URIs") ") // Newline is needed for the enumeration of possible values .long("verb") .next_line_help(true) - .possible_values(&HttpVerb::variants()) + .possible_values(HttpVerb::variants()) .takes_value(true)) .arg(Arg::with_name("wordlist") .display_order(20) @@ -456,7 +459,7 @@ folder as the executable") .long("wordlist") .multiple(true) .next_line_help(true) - .short("w") + .short('w') .takes_value(true) .value_name("wordlist")) .arg(Arg::with_name("extensions") @@ -467,8 +470,8 @@ folder as the executable") .min_values(1) .multiple(true) .next_line_help(true) - .short("x") - .value_delimiter(",") + .short('x') + .value_delimiter(',') .value_name("extensions")) .arg(Arg::with_name("extension_file") .display_order(30) @@ -478,7 +481,7 @@ per line") .long("extension-file") .multiple(true) .next_line_help(true) - .short("X") + .short('X') .value_name("extension-file")) .group(ArgGroup::with_name("extension-options") .args(&["extensions", "extension_file"]) @@ -494,7 +497,7 @@ substituted with the current extension") .display_order(31) .help("Only scan with provided extensions") .requires("extension-options") - .short("f") + .short('f') .long("force-extension")) .arg(Arg::with_name("prefixes") .display_order(30) @@ -504,8 +507,8 @@ substituted with the current extension") .min_values(1) .multiple(true) .next_line_help(true) - .short("p") - .value_delimiter(",")) + .short('p') + .value_delimiter(',')) .arg(Arg::with_name("prefix_file") .display_order(30) .help( @@ -514,7 +517,7 @@ per line") .long("prefix-file") .multiple(true) .next_line_help(true) - .short("P") + .short('P') .value_name("prefix-file")) .arg(Arg::with_name("output_file") .display_order(40) @@ -522,7 +525,7 @@ per line") "Sets the file to write the report to") .long("output-file") .next_line_help(true) - .short("o") + .short('o') .takes_value(true) .visible_alias("oN")) .arg(Arg::with_name("json_file") @@ -582,7 +585,7 @@ username and password in the form "Sets the maximum number of request threads that will be spawned") .long("max-threads") .next_line_help(true) - .short("t") + .short('t') .takes_value(true) .validator(positive_int_check) .value_name("max-threads")) @@ -593,7 +596,7 @@ username and password in the form "The number of threads to run for each folder/extension combo") .long("wordlist-split") .next_line_help(true) - .short("T") + .short('T') .validator(positive_int_check)) .arg(Arg::with_name("throttle") .display_order(61) @@ -601,7 +604,7 @@ username and password in the form "Time each thread will wait between requests, given in milliseconds") .long("throttle") .next_line_help(true) - .short("z") + .short('z') .takes_value(true) .validator(positive_int_check) .value_name("milliseconds")) @@ -627,7 +630,7 @@ username and password in the form "Disable discovered subdirectory scanning") .long("disable-recursion") .next_line_help(true) - .short("r")) + .short('r')) .arg(Arg::with_name("max_recursion_depth") .display_order(80) .help( @@ -643,7 +646,7 @@ recursion") "Scan listable directories") .long("scan-listable") .next_line_help(true) - .short("l") + .short('l') .takes_value(false)) .arg(Arg::with_name("scrape_listable") .display_order(80) @@ -660,7 +663,7 @@ amounts of output") .long("cookie") .multiple(true) .next_line_help(true) - .short("c") + .short('c') .takes_value(true)) .arg(Arg::with_name("header") .display_order(90) @@ -670,7 +673,7 @@ no value must end in a semicolon") .long("header") .multiple(true) .next_line_help(true) - .short("H") + .short('H') .takes_value(true)) .arg(Arg::with_name("user_agent") .display_order(90) @@ -678,17 +681,15 @@ no value must end in a semicolon") "Set the user-agent provided with requests, by default it isn't set") .long("user-agent") .next_line_help(true) - .short("a") + .short('a') .takes_value(true)) - .arg(Arg::with_name("verbose") + .arg(Arg::with_name("verbose").action(ArgAction::Count) .display_order(100) .help( "Increase the verbosity level. Use twice for full verbosity.") .long("verbose") - .multiple(true) .next_line_help(true) - .short("v") - .takes_value(false) + .short('v') .conflicts_with("silent")) .arg(Arg::with_name("silent") .display_order(100) @@ -697,7 +698,7 @@ no value must end in a semicolon") the end.") .long("silent") .next_line_help(true) - .short("S") + .short('S') .takes_value(false)) .arg(Arg::with_name("code_whitelist") .display_order(110) @@ -708,9 +709,9 @@ also disables detection of not found codes") .min_values(1) .multiple(true) .next_line_help(true) - .short("W") + .short('W') .validator(positive_int_check) - .value_delimiter(",")) + .value_delimiter(',')) .arg(Arg::with_name("code_blacklist") .conflicts_with("code_whitelist") .display_order(110) @@ -720,9 +721,9 @@ also disables detection of not found codes") .min_values(1) .multiple(true) .next_line_help(true) - .short("B") + .short('B') .validator(positive_int_check) - .value_delimiter(",")) + .value_delimiter(',')) .arg(Arg::with_name("disable_validator") .display_order(110) .help( @@ -746,7 +747,7 @@ also disables detection of not found codes") .help( "Ignore the certificate validity for HTTPS") .long("ignore-cert") - .short("k")) + .short('k')) .arg(Arg::with_name("show_htaccess") .help( "Enable display of items containing .ht when they return 403 responses") @@ -780,7 +781,7 @@ set to 0 to disable") .multiple(true) .next_line_help(true) .takes_value(true) - .value_delimiter(",")) + .value_delimiter(',')) } /// filetype is one of "txt", "json", and "xml". Returns a filename that is @@ -886,8 +887,8 @@ pub fn get_version_string() -> &'static str { } } -fn url_is_valid(hostname: String) -> Result<(), String> { - let url = Url::parse(hostname.as_str()); +fn url_is_valid(hostname: &str) -> Result<(), String> { + let url = Url::parse(hostname); if let Ok(u) = url { if u.scheme() == "http" || u.scheme() == "https" { Ok(()) @@ -901,7 +902,7 @@ fn url_is_valid(hostname: String) -> Result<(), String> { // Validator for arguments including the --max-threads flag // Ensures that the value is a positive integer (not 0) -fn positive_int_check(value: String) -> Result<(), String> { +fn positive_int_check(value: &str) -> Result<(), String> { let int_val = value.parse::(); if let Ok(max) = int_val { if max > 0 { @@ -913,7 +914,7 @@ fn positive_int_check(value: String) -> Result<(), String> { // Validator for various arguments, ensures that value is a // positive integer, including 0 -fn int_check(value: String) -> Result<(), String> { +fn int_check(value: &str) -> Result<(), String> { let int_val = value.parse::(); match int_val { Ok(_) => Ok(()), @@ -1077,31 +1078,33 @@ mod test { #[test] fn test_int_checks() { let expected_err = "The number given must be a positive integer."; - positive_int_check("1".into()).unwrap(); - positive_int_check(u32::MAX.to_string()).unwrap(); - assert_eq!(positive_int_check("0".into()).unwrap_err(), expected_err); - assert_eq!(positive_int_check("-1".into()).unwrap_err(), expected_err); - assert_eq!( - positive_int_check((u32::MAX as u64 + 1).to_string()).unwrap_err(), - expected_err - ); + positive_int_check("1").unwrap(); + positive_int_check(&u32::MAX.to_string()).unwrap(); + assert_eq!(positive_int_check("0").unwrap_err(), expected_err); + assert_eq!(positive_int_check("-1").unwrap_err(), expected_err); assert_eq!( - positive_int_check("text".into()).unwrap_err(), + positive_int_check(&(u32::MAX as u64 + 1).to_string()).unwrap_err(), expected_err ); - assert_eq!(positive_int_check("".into()).unwrap_err(), expected_err); + assert_eq!(positive_int_check("text").unwrap_err(), expected_err); + assert_eq!(positive_int_check("").unwrap_err(), expected_err); let expected_err = "The number given must be an integer."; - int_check("1".into()).unwrap(); - int_check("0".into()).unwrap(); - int_check(u32::MAX.to_string()).unwrap(); - assert_eq!(int_check("-1".into()).unwrap_err(), expected_err); + int_check("1").unwrap(); + int_check("0").unwrap(); + int_check(&u32::MAX.to_string()).unwrap(); + assert_eq!(int_check("-1").unwrap_err(), expected_err); assert_eq!( - int_check((u32::MAX as u64 + 1).to_string()).unwrap_err(), + int_check(&(u32::MAX as u64 + 1).to_string()).unwrap_err(), expected_err ); - assert_eq!(int_check("text".into()).unwrap_err(), expected_err); - assert_eq!(int_check("".into()).unwrap_err(), expected_err); + assert_eq!(int_check("text").unwrap_err(), expected_err); + assert_eq!(int_check("").unwrap_err(), expected_err); + } + + #[test] + fn verify_app() { + app().debug_assert(); } #[track_caller] @@ -1820,6 +1823,14 @@ mod test { #[test] fn verbosity() { + assert_args( + ["test", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Info, + ..Default::default() + }, + ); assert_args( ["test", "http://some-host", "--verbose"], GlobalOpts { From 7e4e5cd012b0c604811ef4a6ebb66529fba5b48e Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 14:41:55 +0000 Subject: [PATCH 4/7] Fix clap 3-4 deprecation warnings --- Cargo.lock | 44 +++++ Cargo.toml | 3 +- src/arg_parse.rs | 445 ++++++++++++++++++----------------------------- src/wordlist.rs | 10 +- 4 files changed, 223 insertions(+), 279 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 549089c..7e23ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", + "clap_derive", "clap_lex", "indexmap", "once_cell", @@ -136,6 +137,19 @@ dependencies = [ "textwrap", ] +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -1119,6 +1133,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1681,6 +1701,30 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "version_check", +] + [[package]] name = "proc-macro2" version = "0.4.30" diff --git a/Cargo.toml b/Cargo.toml index 2143d8b..ebc88a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,8 @@ description = "Fast directory scanning and scraping tool" [dependencies] curl = "0.4.19" percent-encoding = "2.1" -clap = { version = "3.2.25", features = ["cargo"] } +# clap = { version = "4.5.32", features = ["cargo"] } +clap = { version = "3.2.25", features = ["cargo", "deprecated", "derive"] } select = "0.6" chardet = "0.2.4" encoding = "0.2.33" diff --git a/src/arg_parse.rs b/src/arg_parse.rs index abe927d..993fbc1 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -18,11 +18,11 @@ use crate::wordlist::lines_from_file; use atty::Stream; use clap::{ - arg_enum, crate_version, value_t, App, AppSettings, Arg, ArgAction, - ArgGroup, + builder::EnumValueParser, crate_version, value_parser, Arg, ArgAction, + ArgGroup, Command, ValueEnum, }; use simplelog::LevelFilter; -use std::{ffi::OsString, fmt, process::exit}; +use std::{ffi::OsString, fmt, path::PathBuf, process::exit}; use url::Url; #[derive(Clone, Debug, PartialEq, Eq)] @@ -127,13 +127,11 @@ pub struct ScanOpts { pub scan_403: bool, } -arg_enum! { - #[derive(Copy,Clone, Debug,PartialEq,Eq)] - pub enum HttpVerb { - Get, - Head, - Post - } +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum HttpVerb { + Get, + Head, + Post, } #[expect(clippy::derivable_impls, reason = "Interaction with arg_enum!")] @@ -144,34 +142,42 @@ impl Default for HttpVerb { } /// The supported output file types +#[derive(Copy, Clone)] enum FileTypes { Txt, Json, Xml, } +impl From for &'static str { + fn from(file: FileTypes) -> Self { + match file { + FileTypes::Txt => "txt", + FileTypes::Json => "json", + FileTypes::Xml => "xml", + } + } +} + #[allow(clippy::cognitive_complexity)] pub fn get_args(args: ArgsIter) -> GlobalOpts where ArgsIter: IntoIterator, ArgsIter::Item: Into + Clone, { - // Defines all the command line arguments with the Clap module - let args = app().get_matches_from(args); + // Mutable to allow extractors to move CLI arguments and avoid + // unnecessary clones + let mut args = app().get_matches_from(args); let mut hostnames: Vec = Vec::new(); // Get from host arguments - if let Some(host) = args.value_of("host") { - if let Ok(host) = Url::parse(host) { - hostnames.push(host); - } else { - println!("Invaid URL: {}", host); - } + if let Some(host) = args.remove_one::("host") { + hostnames.push(host); } - if let Some(host_files) = args.values_of("host_file") { + if let Some(host_files) = args.remove_many::("host_file") { for host_file in host_files { - let hosts = lines_from_file(&String::from(host_file)); + let hosts = lines_from_file(&host_file); for hostname in hosts { if url_is_valid(&hostname).is_ok() { if let Ok(host) = Url::parse(hostname.as_str()) { @@ -183,12 +189,8 @@ where } } } - if let Some(extra_hosts) = args.values_of("extra_hosts") { - for hostname in extra_hosts { - if let Ok(host) = Url::parse(hostname) { - hostnames.push(host); - } - } + if let Some(extra_hosts) = args.remove_many::("extra_hosts") { + hostnames.extend(extra_hosts); } if hostnames.is_empty() { @@ -200,20 +202,12 @@ where // Parse wordlist file names into a vector let wordlists: Option> = - if let Some(wordlist_files) = args.values_of("wordlist") { - let mut wordlists_vec = Vec::new(); - for wordlist_file in wordlist_files { - wordlists_vec.push(String::from(wordlist_file)); - } - Some(wordlists_vec) - } else { - None - }; + args.remove_many("wordlist").map(Iterator::collect); // Check for proxy related flags let proxy_enabled; let proxy_address; - if let Some(proxy_addr) = args.value_of("proxy") { + if let Some(proxy_addr) = args.remove_one("proxy") { proxy_enabled = true; proxy_address = proxy_addr; if proxy_address == "http://localhost:8080" { @@ -221,80 +215,61 @@ where "You could use the --burp flag instead of the --proxy flag!" ); } - } else if args.is_present("burp") { + } else if args.get_flag("burp") { proxy_enabled = true; - proxy_address = "http://localhost:8080"; - } else if args.is_present("no_proxy") { + proxy_address = "http://localhost:8080".into(); + } else if args.get_flag("no_proxy") { proxy_enabled = true; - proxy_address = ""; + proxy_address = String::new(); } else { proxy_enabled = false; - proxy_address = ""; + proxy_address = String::new(); } - let proxy_address = String::from(proxy_address); // Read provided cookie values into a vector - let cookies: Option = - if let Some(cookies) = args.values_of("cookie") { - let mut temp_cookies: Vec = Vec::new(); - for cookie in cookies { - temp_cookies.push(String::from(cookie)); - } - Some(temp_cookies.join("; ")) - } else { - None - }; + let cookies: Option = args + .remove_many("cookie") + .map(Iterator::collect::>) + .map(|cookies| cookies.join("; ")); // Read provided headers into a vector let headers: Option> = - if let Some(headers) = args.values_of("header") { - let mut temp_headers: Vec = Vec::new(); - for header in headers { - temp_headers.push(String::from(header)); - } - Some(temp_headers) - } else { - None - }; + args.remove_many("header").map(Iterator::collect); let mut whitelist = false; let mut code_list: Vec = Vec::new(); - if let Some(whitelist_values) = args.values_of("code_whitelist") { + if let Some(whitelist_values) = args.remove_many::("code_whitelist") { whitelist = true; - for code in whitelist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } - } else if let Some(blacklist_values) = args.values_of("code_blacklist") { + code_list.extend(whitelist_values); + } else if let Some(blacklist_values) = + args.remove_many::("code_blacklist") + { whitelist = false; - for code in blacklist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } + code_list.extend(blacklist_values); } - let mut max_recursion_depth = None; - if args.is_present("disable_recursion") { - max_recursion_depth = Some(0); - } else if let Some(depth) = args.value_of("max_recursion_depth") { - max_recursion_depth = - Some(depth.parse::().expect("Recursion depth is an integer")); - } + let max_recursion_depth = if args.get_flag("disable_recursion") { + Some(0) + } else { + args.remove_one("max_recursion_depth") + }; let mut scan_opts = ScanOpts { scan_401: false, scan_403: false, }; - if args.is_present("scan_401") || (whitelist && code_list.contains(&401)) { + if args.get_flag("scan_401") || (whitelist && code_list.contains(&401)) { scan_opts.scan_401 = true; } - if args.is_present("scan_403") || (whitelist && code_list.contains(&403)) { + if args.get_flag("scan_403") || (whitelist && code_list.contains(&403)) { scan_opts.scan_403 = true; } // Configure the logging level. The silent flag overrides any // verbose flags in use. - let log_level = if args.is_present("silent") { + let log_level = if args.get_flag("silent") { LevelFilter::Warn } else { match args.get_count("verbose") { @@ -308,63 +283,48 @@ where GlobalOpts { hostnames, wordlist_files: wordlists, - prefixes: load_modifiers(&args, "prefixes"), - extensions: load_modifiers(&args, "extensions"), - extension_substitution: args.is_present("extension_substitution"), + prefixes: load_modifiers(&mut args, "prefixes"), + extensions: load_modifiers(&mut args, "extensions"), + extension_substitution: args.get_flag("extension_substitution"), max_threads: args - .value_of("max_threads") - .expect("Max threads is set") - .parse::() - .expect("Max threads is an integer"), + .remove_one("max_threads") + .expect("Max threads is set"), proxy_enabled, proxy_address, proxy_auth_enabled: false, - ignore_cert: args.is_present("ignore_cert"), - show_htaccess: args.is_present("show_htaccess"), - throttle: if let Some(throttle) = args.value_of("throttle") { - throttle.parse::().expect("Throttle is an integer") - } else { - 0 - }, + ignore_cert: args.get_flag("ignore_cert"), + show_htaccess: args.get_flag("show_htaccess"), + throttle: args.remove_one("throttle").unwrap_or_default(), max_recursion_depth, - user_agent: args.value_of("user_agent").map(String::from), + user_agent: args.remove_one("user_agent"), // Dependency between username and password is handled by Clap - username: args.value_of("username").map(String::from), + username: args.remove_one("username"), // Dependency between username and password is handled by Clap - password: args.value_of("password").map(String::from), + password: args.remove_one("password"), output_file: filename_from_args(&args, FileTypes::Txt), json_file: filename_from_args(&args, FileTypes::Json), xml_file: filename_from_args(&args, FileTypes::Xml), - timeout: args - .value_of("timeout") - .expect("Timeout is set") - .parse::() - .expect("Timeout is an integer"), + timeout: args.remove_one("timeout").expect("Timeout is set"), max_errors: args - .value_of("max_errors") - .expect("Max errors is set") - .parse::() + .remove_one::("max_errors") .expect("Max errors is an integer"), wordlist_split: args - .value_of("wordlist_split") - .expect("Wordlist split is set") - .parse::() - .expect("Wordlist split is an integer"), - scan_listable: args.is_present("scan_listable"), + .remove_one("wordlist_split") + .expect("Wordlist split is set"), + scan_listable: args.get_flag("scan_listable"), cookies, headers, - scrape_listable: args.is_present("scrape_listable"), + scrape_listable: args.get_flag("scrape_listable"), whitelist, code_list, is_terminal: atty::is(Stream::Stdout), - no_color: args.is_present("no_color"), - disable_validator: args.is_present("disable_validator"), - http_verb: value_t!(args.value_of("http_verb"), HttpVerb) - .expect("Must be valid HTTP verb"), + no_color: args.get_flag("no_color"), + disable_validator: args.get_flag("disable_validator"), + http_verb: *args.get_one("http_verb").expect("Must be valid HTTP verb"), scan_opts, log_level, length_blacklist: if let Some(lengths) = - args.values_of("length_blacklist") + args.get_many("length_blacklist") { length_blacklist_parse(lengths) } else { @@ -373,12 +333,13 @@ where } } -fn app() -> App<'static> { +// Defines all the command line arguments with the Clap module +fn app() -> Command<'static> { // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. let version_string = get_version_string(); - App::new("Dirble") + Command::new("Dirble") .version(version_string) .author( "Developed by Izzy Whistlecroft \ @@ -400,8 +361,8 @@ EXAMPLE USE: dirble [address] -X wordlists/web.lst -U uri-list.txt\n - Providing multiple hosts to scan via command line: dirble [address] -u [address] -u [address]") - .setting(AppSettings::ArgRequiredElseHelp) - .arg(Arg::with_name("host") + .arg_required_else_help(true) + .arg(Arg::new("host") .display_order(10) .help( "The URI of the host to scan, optionally supports basic auth with @@ -409,117 +370,112 @@ http://user:pass@host:port") .index(1) .next_line_help(true) .takes_value(true) - .validator(url_is_valid) + .value_parser(value_parser!(Url)) .value_name("uri")) - .arg(Arg::with_name("extra_hosts") + .arg(Arg::new("extra_hosts").action(ArgAction::Append) .alias("host") .display_order(10) .help( "Additional hosts to scan") .long("uri") - .multiple(true) .next_line_help(true) .short('u') .takes_value(true) - .validator(url_is_valid) + .value_parser(value_parser!(Url)) .value_name("uri") .visible_alias("url")) - .arg(Arg::with_name("host_file") + .arg(Arg::new("host_file").action(ArgAction::Append) .alias("host-file") .display_order(10) .help( "The filename of a file containing a list of URIs to scan - cookies and headers set will be applied to all URIs") .long("uri-file") - .multiple(true) .next_line_help(true) .short('U') .takes_value(true) .value_name("uri-file") + .value_parser(value_parser!(PathBuf)) .visible_alias("url-file")) - .group(ArgGroup::with_name("hosts") + .group(ArgGroup::new("hosts") .args(&["host", "host_file", "extra_hosts"]) .multiple(true) .required(true)) - .arg(Arg::with_name("http_verb") - .default_value("Get") + .arg(Arg::new("http_verb") + .default_value("get") .display_order(11) .help( "Specify which HTTP verb to use ") // Newline is needed for the enumeration of possible values + .ignore_case(true) .long("verb") .next_line_help(true) - .possible_values(HttpVerb::variants()) + .value_parser(EnumValueParser::::new()) .takes_value(true)) - .arg(Arg::with_name("wordlist") + .arg(Arg::new("wordlist").action(ArgAction::Append) .display_order(20) .help( "Sets which wordlist to use, defaults to dirble_wordlist.txt in the same folder as the executable") .long("wordlist") - .multiple(true) .next_line_help(true) .short('w') .takes_value(true) .value_name("wordlist")) - .arg(Arg::with_name("extensions") + .arg(Arg::new("extensions").action(ArgAction::Append) .display_order(30) .help( "Provides comma separated extensions to extend queries with") .long("extensions") .min_values(1) - .multiple(true) .next_line_help(true) .short('x') .value_delimiter(',') .value_name("extensions")) - .arg(Arg::with_name("extension_file") + .arg(Arg::new("extension_file").action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one per line") .long("extension-file") - .multiple(true) .next_line_help(true) - .short('X') + .short('X').value_parser(value_parser!(PathBuf)) .value_name("extension-file")) - .group(ArgGroup::with_name("extension-options") + .group(ArgGroup::new("extension-options") .args(&["extensions", "extension_file"]) .multiple(true)) - .arg(Arg::with_name("extension_substitution") + .arg(Arg::new("extension_substitution").action(ArgAction::SetTrue) .display_order(31) .help( "Indicates whether the string \"%EXT%\" in a wordlist file should be substituted with the current extension") .long("ext-sub") .requires("extension-options")) - .arg(Arg::with_name("force_extension") + .arg(Arg::new("force_extension").action(ArgAction::SetTrue) .display_order(31) .help("Only scan with provided extensions") .requires("extension-options") .short('f') .long("force-extension")) - .arg(Arg::with_name("prefixes") + .arg(Arg::new("prefixes").action(ArgAction::Append) .display_order(30) .help( "Provides comma separated prefixes to extend queries with") .long("prefixes") .min_values(1) - .multiple(true) .next_line_help(true) .short('p') .value_delimiter(',')) - .arg(Arg::with_name("prefix_file") + .arg(Arg::new("prefix_file").action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one per line") .long("prefix-file") - .multiple(true) .next_line_help(true) - .short('P') + .short('P').value_parser(value_parser!(PathBuf)) .value_name("prefix-file")) - .arg(Arg::with_name("output_file") + .arg(Arg::new("output_file") .display_order(40) .help( "Sets the file to write the report to") @@ -528,7 +484,7 @@ per line") .short('o') .takes_value(true) .visible_alias("oN")) - .arg(Arg::with_name("json_file") + .arg(Arg::new("json_file") .display_order(40) .help( "Sets a file to write JSON output to") @@ -536,7 +492,7 @@ per line") .next_line_help(true) .takes_value(true) .visible_alias("oJ")) - .arg(Arg::with_name("xml_file") + .arg(Arg::new("xml_file") .display_order(40) .help( "Sets a file to write XML output to") @@ -544,7 +500,7 @@ per line") .next_line_help(true) .takes_value(true) .visible_alias("oX")) - .arg(Arg::with_name("output_all") + .arg(Arg::new("output_all") .display_order(41) .help( "Stores all output types respectively as .txt, .json and .xml") @@ -552,7 +508,7 @@ per line") .next_line_help(true) .takes_value(true) .visible_alias("oA")) - .arg(Arg::with_name("proxy") + .arg(Arg::new("proxy") .display_order(50) .help( "The proxy address to use, including type and port, can also include a @@ -560,7 +516,7 @@ username and password in the form \"http://username:password@proxy_url:proxy_port\"") .long("proxy") .value_name("proxy")) - .arg(Arg::with_name("burp") + .arg(Arg::new("burp").action(ArgAction::SetTrue) .conflicts_with("proxy") .display_order(50) .help( @@ -568,8 +524,8 @@ username and password in the form (http://localhost:8080)") .long("burp") .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("no_proxy") + ) + .arg(Arg::new("no_proxy").action(ArgAction::SetTrue) .conflicts_with("burp") .conflicts_with("proxy") .display_order(50) @@ -578,7 +534,7 @@ username and password in the form .long("no-proxy") .next_line_help(true) .takes_value(false)) - .arg(Arg::with_name("max_threads") + .arg(Arg::new("max_threads") .default_value("10") .display_order(60) .help( @@ -587,9 +543,9 @@ username and password in the form .next_line_help(true) .short('t') .takes_value(true) - .validator(positive_int_check) + .value_parser(value_parser!(u32).range(1..)) .value_name("max-threads")) - .arg(Arg::with_name("wordlist_split") + .arg(Arg::new("wordlist_split") .default_value("3") .display_order(60) .help( @@ -597,8 +553,8 @@ username and password in the form .long("wordlist-split") .next_line_help(true) .short('T') - .validator(positive_int_check)) - .arg(Arg::with_name("throttle") + .value_parser(value_parser!(u32))) + .arg(Arg::new("throttle") .display_order(61) .help( "Time each thread will wait between requests, given in milliseconds") @@ -606,9 +562,9 @@ username and password in the form .next_line_help(true) .short('z') .takes_value(true) - .validator(positive_int_check) + .value_parser(value_parser!(u32)) .value_name("milliseconds")) - .arg(Arg::with_name("username") + .arg(Arg::new("username") .display_order(70) .help( "Sets the username to authenticate with") @@ -616,7 +572,7 @@ username and password in the form .next_line_help(true) .requires("password") .takes_value(true)) - .arg(Arg::with_name("password") + .arg(Arg::new("password") .display_order(71) .help( "Sets the password to authenticate with") @@ -624,14 +580,14 @@ username and password in the form .next_line_help(true) .requires("username") .takes_value(true)) - .arg(Arg::with_name("disable_recursion") + .arg(Arg::new("disable_recursion").action(ArgAction::SetTrue) .display_order(80) .help( "Disable discovered subdirectory scanning") .long("disable-recursion") .next_line_help(true) .short('r')) - .arg(Arg::with_name("max_recursion_depth") + .arg(Arg::new("max_recursion_depth") .display_order(80) .help( "Sets the maximum directory depth to recurse to, 0 will disable @@ -639,43 +595,41 @@ recursion") .long("max-recursion-depth") .next_line_help(true) .takes_value(true) - .validator(int_check)) - .arg(Arg::with_name("scan_listable") + .value_parser(value_parser!(i32))) + .arg(Arg::new("scan_listable").action(ArgAction::SetTrue) .display_order(80) .help( "Scan listable directories") .long("scan-listable") .next_line_help(true) .short('l') - .takes_value(false)) - .arg(Arg::with_name("scrape_listable") + ) + .arg(Arg::new("scrape_listable").action(ArgAction::SetTrue) .display_order(80) .help( "Enable scraping of listable directories for urls, often produces large amounts of output") .long("scrape-listable") .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("cookie") + ) + .arg(Arg::new("cookie").action(ArgAction::Append) .display_order(90) .help( "Provide a cookie in the form \"name=value\", can be used multiple times") .long("cookie") - .multiple(true) .next_line_help(true) .short('c') .takes_value(true)) - .arg(Arg::with_name("header") + .arg(Arg::new("header").action(ArgAction::Append) .display_order(90) .help( "Provide an arbitrary header in the form \"header:value\" - headers with no value must end in a semicolon") .long("header") - .multiple(true) .next_line_help(true) .short('H') .takes_value(true)) - .arg(Arg::with_name("user_agent") + .arg(Arg::new("user_agent") .display_order(90) .help( "Set the user-agent provided with requests, by default it isn't set") @@ -683,7 +637,7 @@ no value must end in a semicolon") .next_line_help(true) .short('a') .takes_value(true)) - .arg(Arg::with_name("verbose").action(ArgAction::Count) + .arg(Arg::new("verbose").action(ArgAction::Count) .display_order(100) .help( "Increase the verbosity level. Use twice for full verbosity.") @@ -691,94 +645,90 @@ no value must end in a semicolon") .next_line_help(true) .short('v') .conflicts_with("silent")) - .arg(Arg::with_name("silent") + .arg(Arg::new("silent").action(ArgAction::SetTrue) .display_order(100) .help( "Don't output information during the scan, only output the report at the end.") .long("silent") .next_line_help(true) - .short('S') - .takes_value(false)) - .arg(Arg::with_name("code_whitelist") + .short('S')) + .arg(Arg::new("code_whitelist").action(ArgAction::Append) .display_order(110) .help( "Provide a comma separated list of response codes to show in output, also disables detection of not found codes") .long("code-whitelist") .min_values(1) - .multiple(true) .next_line_help(true) .short('W') - .validator(positive_int_check) - .value_delimiter(',')) - .arg(Arg::with_name("code_blacklist") + .value_delimiter(',') + .value_parser(value_parser!(u32))) + .arg(Arg::new("code_blacklist") + .action(ArgAction::Append) .conflicts_with("code_whitelist") .display_order(110) .help( "Provide a comma separated list of response codes to not show in output") .long("code-blacklist") .min_values(1) - .multiple(true) .next_line_help(true) .short('B') - .validator(positive_int_check) + .value_parser(value_parser!(u32)) .value_delimiter(',')) - .arg(Arg::with_name("disable_validator") + .arg(Arg::new("disable_validator").action(ArgAction::SetTrue) .display_order(110) .help( "Disable automatic detection of not found codes") .long("disable-validator") - .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("scan_401") + .next_line_help(true)) + .arg(Arg::new("scan_401").action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders even if they return 401 - Unauthorized frequently") .long("scan-401") .next_line_help(true)) - .arg(Arg::with_name("scan_403") + .arg(Arg::new("scan_403").action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders if they return 403 - Forbidden frequently") .long("scan-403") .next_line_help(true)) - .arg(Arg::with_name("ignore_cert") + .arg(Arg::new("ignore_cert").action(ArgAction::SetTrue) .help( "Ignore the certificate validity for HTTPS") .long("ignore-cert") .short('k')) - .arg(Arg::with_name("show_htaccess") + .arg(Arg::new("show_htaccess").action(ArgAction::SetTrue) .help( "Enable display of items containing .ht when they return 403 responses") .long("show-htaccess") .next_line_help(true)) - .arg(Arg::with_name("timeout") + .arg(Arg::new("timeout") .default_value("5") .help( "Maximum time to wait for a response before giving up, given in seconds\n") .long("timeout") .next_line_help(true) - .validator(positive_int_check)) - .arg(Arg::with_name("max_errors") + .value_parser(value_parser!(u32))) + .arg(Arg::new("max_errors") .default_value("5") .help( "The number of consecutive errors a thread can have before it exits, set to 0 to disable") .long("max-errors") .next_line_help(true) - .validator(int_check)) - .arg(Arg::with_name("no_color") + .value_parser(value_parser!(u32))) + .arg(Arg::new("no_color").action(ArgAction::SetTrue) .alias("no-colour") .help("Disable coloring of terminal output") .long("no-color") .next_line_help(true)) - .arg(Arg::with_name("length_blacklist") + .arg(Arg::new("length_blacklist").action(ArgAction::Append) .help( "Specify length ranges to hide, e.g. --hide-lengths 348,500-700") .long("hide-lengths") .min_values(1) - .multiple(true) .next_line_help(true) .takes_value(true) .value_delimiter(',')) @@ -793,35 +743,33 @@ fn filename_from_args( args: &clap::ArgMatches, filetype: FileTypes, ) -> Option { - let extension; - use FileTypes::*; match filetype { - Txt => { - extension = "txt"; - if let Some(output_file) = args.value_of("output_file") { - return Some(String::from(output_file)); + FileTypes::Txt => { + if let Some(output_file) = args.get_one::("output_file") { + return Some(output_file.to_string()); } } - Json => { - extension = "json"; - if let Some(json_file) = args.value_of("json_file") { - return Some(String::from(json_file)); + FileTypes::Json => { + if let Some(json_file) = args.get_one::("json_file") { + return Some(json_file.to_string()); } } - Xml => { - extension = "xml"; - if let Some(xml_file) = args.value_of("xml_file") { - return Some(String::from(xml_file)); + FileTypes::Xml => { + if let Some(xml_file) = args.get_one::("xml_file") { + return Some(xml_file.to_string()); } } } - args.value_of("output_all") - .map(|output_all_prefix| format!("{}.{}", output_all_prefix, extension)) + // This function is called once for each filetype, so we don't remove + // the arg from the matcher + args.get_one::("output_all") + .map(|output_all_prefix| { + format!("{}.{}", output_all_prefix, <&'static str>::from(filetype)) + }) } -#[inline] -fn load_modifiers(args: &clap::ArgMatches, mod_type: &str) -> Vec { +fn load_modifiers(args: &mut clap::ArgMatches, mod_type: &str) -> Vec { let singular_arg; let file_arg; match mod_type { @@ -839,20 +787,16 @@ fn load_modifiers(args: &clap::ArgMatches, mod_type: &str) -> Vec { let mut modifiers = vec![]; - if !args.is_present("force_extension") || mod_type == "prefixes" { + if !args.get_flag("force_extension") || mod_type == "prefixes" { modifiers.push(String::from("")); } - if let Some(singular_args) = args.values_of(singular_arg) { - for modifier in singular_args { - modifiers.push(String::from(modifier)); - } + if let Some(singular_args) = args.remove_many(singular_arg) { + modifiers.extend(singular_args); } - if let Some(file_args) = args.values_of(file_arg) { + if let Some(file_args) = args.remove_many::(&file_arg) { for filename in file_args { - for modifier in lines_from_file(&String::from(filename)) { - modifiers.push(modifier); - } + modifiers.extend(lines_from_file(&filename)); } } @@ -900,29 +844,9 @@ fn url_is_valid(hostname: &str) -> Result<(), String> { } } -// Validator for arguments including the --max-threads flag -// Ensures that the value is a positive integer (not 0) -fn positive_int_check(value: &str) -> Result<(), String> { - let int_val = value.parse::(); - if let Ok(max) = int_val { - if max > 0 { - return Ok(()); - } - } - Err(String::from("The number given must be a positive integer.")) -} - -// Validator for various arguments, ensures that value is a -// positive integer, including 0 -fn int_check(value: &str) -> Result<(), String> { - let int_val = value.parse::(); - match int_val { - Ok(_) => Ok(()), - Err(_) => Err(String::from("The number given must be an integer.")), - } -} - -fn length_blacklist_parse(blacklist_inputs: clap::Values) -> LengthRanges { +fn length_blacklist_parse( + blacklist_inputs: clap::parser::ValuesRef, +) -> LengthRanges { let mut length_vector: Vec = Vec::with_capacity(blacklist_inputs.len()); @@ -1075,33 +999,6 @@ mod test { assert!(!ranges.contains(19)); } - #[test] - fn test_int_checks() { - let expected_err = "The number given must be a positive integer."; - positive_int_check("1").unwrap(); - positive_int_check(&u32::MAX.to_string()).unwrap(); - assert_eq!(positive_int_check("0").unwrap_err(), expected_err); - assert_eq!(positive_int_check("-1").unwrap_err(), expected_err); - assert_eq!( - positive_int_check(&(u32::MAX as u64 + 1).to_string()).unwrap_err(), - expected_err - ); - assert_eq!(positive_int_check("text").unwrap_err(), expected_err); - assert_eq!(positive_int_check("").unwrap_err(), expected_err); - - let expected_err = "The number given must be an integer."; - int_check("1").unwrap(); - int_check("0").unwrap(); - int_check(&u32::MAX.to_string()).unwrap(); - assert_eq!(int_check("-1").unwrap_err(), expected_err); - assert_eq!( - int_check(&(u32::MAX as u64 + 1).to_string()).unwrap_err(), - expected_err - ); - assert_eq!(int_check("text").unwrap_err(), expected_err); - assert_eq!(int_check("").unwrap_err(), expected_err); - } - #[test] fn verify_app() { app().debug_assert(); diff --git a/src/wordlist.rs b/src/wordlist.rs index 638d4cb..416ab82 100644 --- a/src/wordlist.rs +++ b/src/wordlist.rs @@ -17,8 +17,8 @@ use crate::validator_thread::TargetValidator; use chardet::{charset2encoding, detect}; -use encoding::{DecoderTrap, label::encoding_from_whatwg_label}; -use std::{fs, sync::Arc}; +use encoding::{label::encoding_from_whatwg_label, DecoderTrap}; +use std::{fs, path::Path, sync::Arc}; use url::Url; // Struct for a UriGenerator, it needs the hostname, the suffix to @@ -105,7 +105,9 @@ impl Iterator for UriGenerator { } // Function used to read in lines from the wordlist file -pub fn lines_from_file(filename: &str) -> Vec { +pub fn lines_from_file(filename: impl AsRef) -> Vec { + let filename = filename.as_ref(); + // Read the raw file in as a vector of bytes let reader = fs::read(filename).unwrap(); @@ -127,7 +129,7 @@ pub fn lines_from_file(filename: &str) -> Vec { None => { panic!( "Error detecting file encoding of {} - is the file empty?", - filename + filename.display(), ); } } From cff7b9ada9e5637e86c8ffce0f11f33078e62527 Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 14:51:26 +0000 Subject: [PATCH 5/7] Clap 3 to 4 --- Cargo.lock | 197 ++++++++++++++++++++++++----------------------- Cargo.toml | 3 +- src/arg_parse.rs | 77 ++++++++---------- 3 files changed, 134 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e23ab5..9d5a595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.97" @@ -70,12 +120,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.0" @@ -122,42 +166,43 @@ checksum = "1a48563284b67c003ba0fb7243c87fab68885e1532c605704228a80238512e31" [[package]] name = "clap" -version = "3.2.25" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" dependencies = [ - "atty", - "bitflags 1.3.2", + "clap_builder", "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "indexmap", - "once_cell", - "strsim 0.10.0", - "termcolor", - "textwrap", + "strsim", ] [[package]] name = "clap_derive" -version = "3.2.25" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", - "proc-macro-error", "proc-macro2 1.0.94", "quote 1.0.40", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clru" @@ -165,6 +210,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "colored" version = "3.0.0" @@ -274,7 +325,7 @@ dependencies = [ "ident_case", "proc-macro2 1.0.94", "quote 1.0.40", - "strsim 0.11.1", + "strsim", "syn 2.0.100", ] @@ -673,7 +724,7 @@ version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-path", "libc", @@ -756,7 +807,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-features", "gix-path", @@ -779,7 +830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" dependencies = [ "gix-hash", - "hashbrown 0.14.5", + "hashbrown", "parking_lot", ] @@ -789,7 +840,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "filetime", "fnv", @@ -802,7 +853,7 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown 0.14.5", + "hashbrown", "itoa", "libc", "memmap2", @@ -978,7 +1029,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e1ddc474405a68d2ce8485705dd72fe6ce959f2f5fe718601ead5da2c8f9e7" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-commitgraph", "gix-date", @@ -1011,7 +1062,7 @@ version = "0.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875" dependencies = [ - "bitflags 2.9.0", + "bitflags", "gix-path", "libc", "windows-sys 0.52.0", @@ -1072,7 +1123,7 @@ version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed47d648619e23e93f971d2bba0d10c1100e54ef95d2981d609907a8cabac89" dependencies = [ - "bitflags 2.9.0", + "bitflags", "gix-commitgraph", "gix-date", "gix-hash", @@ -1117,12 +1168,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1135,9 +1180,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1317,14 +1362,10 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" @@ -1373,7 +1414,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags", "libc", "redox_syscall", ] @@ -1503,7 +1544,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1557,12 +1598,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1701,30 +1736,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2 1.0.94", - "quote 1.0.40", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2 1.0.94", - "quote 1.0.40", - "version_check", -] - [[package]] name = "proc-macro2" version = "0.4.30" @@ -1857,7 +1868,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -1872,7 +1883,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1885,7 +1896,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ - "bitflags 2.9.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.2", @@ -2103,12 +2114,6 @@ dependencies = [ "quote 1.0.40", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -2206,12 +2211,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" - [[package]] name = "thiserror" version = "2.0.12" @@ -2346,6 +2345,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2604,7 +2609,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ebc88a1..17ee143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ description = "Fast directory scanning and scraping tool" [dependencies] curl = "0.4.19" percent-encoding = "2.1" -# clap = { version = "4.5.32", features = ["cargo"] } -clap = { version = "3.2.25", features = ["cargo", "deprecated", "derive"] } +clap = { version = "4.5.32", features = ["cargo", "derive"] } select = "0.6" chardet = "0.2.4" encoding = "0.2.33" diff --git a/src/arg_parse.rs b/src/arg_parse.rs index 993fbc1..c847ab9 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -334,7 +334,7 @@ where } // Defines all the command line arguments with the Clap module -fn app() -> Command<'static> { +fn app() -> Command { // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. @@ -362,14 +362,13 @@ EXAMPLE USE: - Providing multiple hosts to scan via command line: dirble [address] -u [address] -u [address]") .arg_required_else_help(true) - .arg(Arg::new("host") + .arg(Arg::new("host").action(ArgAction::Set) .display_order(10) .help( "The URI of the host to scan, optionally supports basic auth with http://user:pass@host:port") .index(1) .next_line_help(true) - .takes_value(true) .value_parser(value_parser!(Url)) .value_name("uri")) .arg(Arg::new("extra_hosts").action(ArgAction::Append) @@ -380,7 +379,6 @@ http://user:pass@host:port") .long("uri") .next_line_help(true) .short('u') - .takes_value(true) .value_parser(value_parser!(Url)) .value_name("uri") .visible_alias("url")) @@ -393,15 +391,14 @@ headers set will be applied to all URIs") .long("uri-file") .next_line_help(true) .short('U') - .takes_value(true) .value_name("uri-file") .value_parser(value_parser!(PathBuf)) .visible_alias("url-file")) .group(ArgGroup::new("hosts") - .args(&["host", "host_file", "extra_hosts"]) + .args(["host", "host_file", "extra_hosts"]) .multiple(true) .required(true)) - .arg(Arg::new("http_verb") + .arg(Arg::new("http_verb").action(ArgAction::Set) .default_value("get") .display_order(11) .help( @@ -410,8 +407,7 @@ headers set will be applied to all URIs") .ignore_case(true) .long("verb") .next_line_help(true) - .value_parser(EnumValueParser::::new()) - .takes_value(true)) + .value_parser(EnumValueParser::::new())) .arg(Arg::new("wordlist").action(ArgAction::Append) .display_order(20) .help( @@ -420,14 +416,13 @@ folder as the executable") .long("wordlist") .next_line_help(true) .short('w') - .takes_value(true) .value_name("wordlist")) .arg(Arg::new("extensions").action(ArgAction::Append) .display_order(30) .help( "Provides comma separated extensions to extend queries with") .long("extensions") - .min_values(1) + .num_args(1..) .next_line_help(true) .short('x') .value_delimiter(',') @@ -442,7 +437,7 @@ per line") .short('X').value_parser(value_parser!(PathBuf)) .value_name("extension-file")) .group(ArgGroup::new("extension-options") - .args(&["extensions", "extension_file"]) + .args(["extensions", "extension_file"]) .multiple(true)) .arg(Arg::new("extension_substitution").action(ArgAction::SetTrue) .display_order(31) @@ -462,7 +457,7 @@ substituted with the current extension") .help( "Provides comma separated prefixes to extend queries with") .long("prefixes") - .min_values(1) + .num_args(1..) .next_line_help(true) .short('p') .value_delimiter(',')) @@ -475,39 +470,35 @@ per line") .next_line_help(true) .short('P').value_parser(value_parser!(PathBuf)) .value_name("prefix-file")) - .arg(Arg::new("output_file") + .arg(Arg::new("output_file").action(ArgAction::Set) .display_order(40) .help( "Sets the file to write the report to") .long("output-file") .next_line_help(true) .short('o') - .takes_value(true) .visible_alias("oN")) - .arg(Arg::new("json_file") + .arg(Arg::new("json_file").action(ArgAction::Set) .display_order(40) .help( "Sets a file to write JSON output to") .long("json-file") .next_line_help(true) - .takes_value(true) - .visible_alias("oJ")) - .arg(Arg::new("xml_file") + .visible_alias("oJ")) + .arg(Arg::new("xml_file").action(ArgAction::Set) .display_order(40) .help( "Sets a file to write XML output to") .long("xml-file") .next_line_help(true) - .takes_value(true) .visible_alias("oX")) - .arg(Arg::new("output_all") + .arg(Arg::new("output_all").action(ArgAction::Set) .display_order(41) .help( "Stores all output types respectively as .txt, .json and .xml") .long("output-all") .next_line_help(true) - .takes_value(true) - .visible_alias("oA")) + .visible_alias("oA")) .arg(Arg::new("proxy") .display_order(50) .help( @@ -533,8 +524,8 @@ username and password in the form "Disables proxy use even if there is a system proxy") .long("no-proxy") .next_line_help(true) - .takes_value(false)) - .arg(Arg::new("max_threads") + ) + .arg(Arg::new("max_threads").action(ArgAction::Set) .default_value("10") .display_order(60) .help( @@ -542,7 +533,6 @@ username and password in the form .long("max-threads") .next_line_help(true) .short('t') - .takes_value(true) .value_parser(value_parser!(u32).range(1..)) .value_name("max-threads")) .arg(Arg::new("wordlist_split") @@ -554,32 +544,31 @@ username and password in the form .next_line_help(true) .short('T') .value_parser(value_parser!(u32))) - .arg(Arg::new("throttle") + .arg(Arg::new("throttle").action(ArgAction::Set) .display_order(61) .help( "Time each thread will wait between requests, given in milliseconds") .long("throttle") .next_line_help(true) .short('z') - .takes_value(true) - .value_parser(value_parser!(u32)) + .value_parser(value_parser!(u32)) .value_name("milliseconds")) - .arg(Arg::new("username") + .arg(Arg::new("username").action(ArgAction::Set) .display_order(70) .help( "Sets the username to authenticate with") .long("username") .next_line_help(true) .requires("password") - .takes_value(true)) - .arg(Arg::new("password") + ) + .arg(Arg::new("password").action(ArgAction::Set) .display_order(71) .help( "Sets the password to authenticate with") .long("password") .next_line_help(true) .requires("username") - .takes_value(true)) + ) .arg(Arg::new("disable_recursion").action(ArgAction::SetTrue) .display_order(80) .help( @@ -587,15 +576,14 @@ username and password in the form .long("disable-recursion") .next_line_help(true) .short('r')) - .arg(Arg::new("max_recursion_depth") + .arg(Arg::new("max_recursion_depth").action(ArgAction::Set) .display_order(80) .help( "Sets the maximum directory depth to recurse to, 0 will disable recursion") .long("max-recursion-depth") .next_line_help(true) - .takes_value(true) - .value_parser(value_parser!(i32))) + .value_parser(value_parser!(i32))) .arg(Arg::new("scan_listable").action(ArgAction::SetTrue) .display_order(80) .help( @@ -619,7 +607,7 @@ amounts of output") .long("cookie") .next_line_help(true) .short('c') - .takes_value(true)) + ) .arg(Arg::new("header").action(ArgAction::Append) .display_order(90) .help( @@ -628,15 +616,15 @@ no value must end in a semicolon") .long("header") .next_line_help(true) .short('H') - .takes_value(true)) - .arg(Arg::new("user_agent") + ) + .arg(Arg::new("user_agent").action(ArgAction::Set) .display_order(90) .help( "Set the user-agent provided with requests, by default it isn't set") .long("user-agent") .next_line_help(true) .short('a') - .takes_value(true)) + ) .arg(Arg::new("verbose").action(ArgAction::Count) .display_order(100) .help( @@ -659,7 +647,7 @@ the end.") "Provide a comma separated list of response codes to show in output, also disables detection of not found codes") .long("code-whitelist") - .min_values(1) + .num_args(1..) .next_line_help(true) .short('W') .value_delimiter(',') @@ -671,7 +659,7 @@ also disables detection of not found codes") .help( "Provide a comma separated list of response codes to not show in output") .long("code-blacklist") - .min_values(1) + .num_args(1..) .next_line_help(true) .short('B') .value_parser(value_parser!(u32)) @@ -728,10 +716,9 @@ set to 0 to disable") .help( "Specify length ranges to hide, e.g. --hide-lengths 348,500-700") .long("hide-lengths") - .min_values(1) + .num_args(1..) .next_line_help(true) - .takes_value(true) - .value_delimiter(',')) + .value_delimiter(',')) } /// filetype is one of "txt", "json", and "xml". Returns a filename that is From b097a35fba4af2659e6280ddbdbcf955004f518e Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 14:56:32 +0000 Subject: [PATCH 6/7] Fix CI definition --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36ea32a..39ed26b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ - name: Build +name: Build on: push: From c2fd1fe76f5839cc589a97df76e9812f8096ca11 Mon Sep 17 00:00:00 2001 From: David Young Date: Sun, 23 Mar 2025 14:58:04 +0000 Subject: [PATCH 7/7] fmt --- src/arg_parse.rs | 118 +++++++++++++++++++++++++++++++---------------- src/main.rs | 4 +- src/wordlist.rs | 2 +- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/arg_parse.rs b/src/arg_parse.rs index c847ab9..b87083a 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -18,8 +18,8 @@ use crate::wordlist::lines_from_file; use atty::Stream; use clap::{ - builder::EnumValueParser, crate_version, value_parser, Arg, ArgAction, - ArgGroup, Command, ValueEnum, + Arg, ArgAction, ArgGroup, Command, ValueEnum, builder::EnumValueParser, + crate_version, value_parser, }; use simplelog::LevelFilter; use std::{ffi::OsString, fmt, path::PathBuf, process::exit}; @@ -362,7 +362,8 @@ EXAMPLE USE: - Providing multiple hosts to scan via command line: dirble [address] -u [address] -u [address]") .arg_required_else_help(true) - .arg(Arg::new("host").action(ArgAction::Set) + .arg(Arg::new("host") + .action(ArgAction::Set) .display_order(10) .help( "The URI of the host to scan, optionally supports basic auth with @@ -371,7 +372,8 @@ http://user:pass@host:port") .next_line_help(true) .value_parser(value_parser!(Url)) .value_name("uri")) - .arg(Arg::new("extra_hosts").action(ArgAction::Append) + .arg(Arg::new("extra_hosts") + .action(ArgAction::Append) .alias("host") .display_order(10) .help( @@ -382,7 +384,8 @@ http://user:pass@host:port") .value_parser(value_parser!(Url)) .value_name("uri") .visible_alias("url")) - .arg(Arg::new("host_file").action(ArgAction::Append) + .arg(Arg::new("host_file") + .action(ArgAction::Append) .alias("host-file") .display_order(10) .help( @@ -398,7 +401,8 @@ headers set will be applied to all URIs") .args(["host", "host_file", "extra_hosts"]) .multiple(true) .required(true)) - .arg(Arg::new("http_verb").action(ArgAction::Set) + .arg(Arg::new("http_verb") + .action(ArgAction::Set) .default_value("get") .display_order(11) .help( @@ -408,7 +412,8 @@ headers set will be applied to all URIs") .long("verb") .next_line_help(true) .value_parser(EnumValueParser::::new())) - .arg(Arg::new("wordlist").action(ArgAction::Append) + .arg(Arg::new("wordlist") + .action(ArgAction::Append) .display_order(20) .help( "Sets which wordlist to use, defaults to dirble_wordlist.txt in the same @@ -417,7 +422,8 @@ folder as the executable") .next_line_help(true) .short('w') .value_name("wordlist")) - .arg(Arg::new("extensions").action(ArgAction::Append) + .arg(Arg::new("extensions") + .action(ArgAction::Append) .display_order(30) .help( "Provides comma separated extensions to extend queries with") @@ -427,7 +433,8 @@ folder as the executable") .short('x') .value_delimiter(',') .value_name("extensions")) - .arg(Arg::new("extension_file").action(ArgAction::Append) + .arg(Arg::new("extension_file") + .action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one @@ -439,20 +446,23 @@ per line") .group(ArgGroup::new("extension-options") .args(["extensions", "extension_file"]) .multiple(true)) - .arg(Arg::new("extension_substitution").action(ArgAction::SetTrue) + .arg(Arg::new("extension_substitution") + .action(ArgAction::SetTrue) .display_order(31) .help( "Indicates whether the string \"%EXT%\" in a wordlist file should be substituted with the current extension") .long("ext-sub") .requires("extension-options")) - .arg(Arg::new("force_extension").action(ArgAction::SetTrue) + .arg(Arg::new("force_extension") + .action(ArgAction::SetTrue) .display_order(31) .help("Only scan with provided extensions") .requires("extension-options") .short('f') .long("force-extension")) - .arg(Arg::new("prefixes").action(ArgAction::Append) + .arg(Arg::new("prefixes") + .action(ArgAction::Append) .display_order(30) .help( "Provides comma separated prefixes to extend queries with") @@ -461,7 +471,8 @@ substituted with the current extension") .next_line_help(true) .short('p') .value_delimiter(',')) - .arg(Arg::new("prefix_file").action(ArgAction::Append) + .arg(Arg::new("prefix_file") + .action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one @@ -470,7 +481,8 @@ per line") .next_line_help(true) .short('P').value_parser(value_parser!(PathBuf)) .value_name("prefix-file")) - .arg(Arg::new("output_file").action(ArgAction::Set) + .arg(Arg::new("output_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets the file to write the report to") @@ -478,21 +490,24 @@ per line") .next_line_help(true) .short('o') .visible_alias("oN")) - .arg(Arg::new("json_file").action(ArgAction::Set) + .arg(Arg::new("json_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets a file to write JSON output to") .long("json-file") .next_line_help(true) .visible_alias("oJ")) - .arg(Arg::new("xml_file").action(ArgAction::Set) + .arg(Arg::new("xml_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets a file to write XML output to") .long("xml-file") .next_line_help(true) .visible_alias("oX")) - .arg(Arg::new("output_all").action(ArgAction::Set) + .arg(Arg::new("output_all") + .action(ArgAction::Set) .display_order(41) .help( "Stores all output types respectively as .txt, .json and .xml") @@ -507,7 +522,8 @@ username and password in the form \"http://username:password@proxy_url:proxy_port\"") .long("proxy") .value_name("proxy")) - .arg(Arg::new("burp").action(ArgAction::SetTrue) + .arg(Arg::new("burp") + .action(ArgAction::SetTrue) .conflicts_with("proxy") .display_order(50) .help( @@ -516,7 +532,8 @@ username and password in the form .long("burp") .next_line_help(true) ) - .arg(Arg::new("no_proxy").action(ArgAction::SetTrue) + .arg(Arg::new("no_proxy") + .action(ArgAction::SetTrue) .conflicts_with("burp") .conflicts_with("proxy") .display_order(50) @@ -525,7 +542,8 @@ username and password in the form .long("no-proxy") .next_line_help(true) ) - .arg(Arg::new("max_threads").action(ArgAction::Set) + .arg(Arg::new("max_threads") + .action(ArgAction::Set) .default_value("10") .display_order(60) .help( @@ -544,7 +562,8 @@ username and password in the form .next_line_help(true) .short('T') .value_parser(value_parser!(u32))) - .arg(Arg::new("throttle").action(ArgAction::Set) + .arg(Arg::new("throttle") + .action(ArgAction::Set) .display_order(61) .help( "Time each thread will wait between requests, given in milliseconds") @@ -553,7 +572,8 @@ username and password in the form .short('z') .value_parser(value_parser!(u32)) .value_name("milliseconds")) - .arg(Arg::new("username").action(ArgAction::Set) + .arg(Arg::new("username") + .action(ArgAction::Set) .display_order(70) .help( "Sets the username to authenticate with") @@ -561,7 +581,8 @@ username and password in the form .next_line_help(true) .requires("password") ) - .arg(Arg::new("password").action(ArgAction::Set) + .arg(Arg::new("password") + .action(ArgAction::Set) .display_order(71) .help( "Sets the password to authenticate with") @@ -569,14 +590,16 @@ username and password in the form .next_line_help(true) .requires("username") ) - .arg(Arg::new("disable_recursion").action(ArgAction::SetTrue) + .arg(Arg::new("disable_recursion") + .action(ArgAction::SetTrue) .display_order(80) .help( "Disable discovered subdirectory scanning") .long("disable-recursion") .next_line_help(true) .short('r')) - .arg(Arg::new("max_recursion_depth").action(ArgAction::Set) + .arg(Arg::new("max_recursion_depth") + .action(ArgAction::Set) .display_order(80) .help( "Sets the maximum directory depth to recurse to, 0 will disable @@ -584,7 +607,8 @@ recursion") .long("max-recursion-depth") .next_line_help(true) .value_parser(value_parser!(i32))) - .arg(Arg::new("scan_listable").action(ArgAction::SetTrue) + .arg(Arg::new("scan_listable") + .action(ArgAction::SetTrue) .display_order(80) .help( "Scan listable directories") @@ -592,7 +616,8 @@ recursion") .next_line_help(true) .short('l') ) - .arg(Arg::new("scrape_listable").action(ArgAction::SetTrue) + .arg(Arg::new("scrape_listable") + .action(ArgAction::SetTrue) .display_order(80) .help( "Enable scraping of listable directories for urls, often produces large @@ -600,7 +625,8 @@ amounts of output") .long("scrape-listable") .next_line_help(true) ) - .arg(Arg::new("cookie").action(ArgAction::Append) + .arg(Arg::new("cookie") + .action(ArgAction::Append) .display_order(90) .help( "Provide a cookie in the form \"name=value\", can be used multiple times") @@ -608,7 +634,8 @@ amounts of output") .next_line_help(true) .short('c') ) - .arg(Arg::new("header").action(ArgAction::Append) + .arg(Arg::new("header") + .action(ArgAction::Append) .display_order(90) .help( "Provide an arbitrary header in the form \"header:value\" - headers with @@ -617,7 +644,8 @@ no value must end in a semicolon") .next_line_help(true) .short('H') ) - .arg(Arg::new("user_agent").action(ArgAction::Set) + .arg(Arg::new("user_agent") + .action(ArgAction::Set) .display_order(90) .help( "Set the user-agent provided with requests, by default it isn't set") @@ -625,7 +653,8 @@ no value must end in a semicolon") .next_line_help(true) .short('a') ) - .arg(Arg::new("verbose").action(ArgAction::Count) + .arg(Arg::new("verbose") + .action(ArgAction::Count) .display_order(100) .help( "Increase the verbosity level. Use twice for full verbosity.") @@ -633,7 +662,8 @@ no value must end in a semicolon") .next_line_help(true) .short('v') .conflicts_with("silent")) - .arg(Arg::new("silent").action(ArgAction::SetTrue) + .arg(Arg::new("silent") + .action(ArgAction::SetTrue) .display_order(100) .help( "Don't output information during the scan, only output the report at @@ -641,7 +671,8 @@ the end.") .long("silent") .next_line_help(true) .short('S')) - .arg(Arg::new("code_whitelist").action(ArgAction::Append) + .arg(Arg::new("code_whitelist") + .action(ArgAction::Append) .display_order(110) .help( "Provide a comma separated list of response codes to show in output, @@ -664,30 +695,35 @@ also disables detection of not found codes") .short('B') .value_parser(value_parser!(u32)) .value_delimiter(',')) - .arg(Arg::new("disable_validator").action(ArgAction::SetTrue) + .arg(Arg::new("disable_validator") + .action(ArgAction::SetTrue) .display_order(110) .help( "Disable automatic detection of not found codes") .long("disable-validator") .next_line_help(true)) - .arg(Arg::new("scan_401").action(ArgAction::SetTrue) + .arg(Arg::new("scan_401") + .action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders even if they return 401 - Unauthorized frequently") .long("scan-401") .next_line_help(true)) - .arg(Arg::new("scan_403").action(ArgAction::SetTrue) + .arg(Arg::new("scan_403") + .action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders if they return 403 - Forbidden frequently") .long("scan-403") .next_line_help(true)) - .arg(Arg::new("ignore_cert").action(ArgAction::SetTrue) + .arg(Arg::new("ignore_cert") + .action(ArgAction::SetTrue) .help( "Ignore the certificate validity for HTTPS") .long("ignore-cert") .short('k')) - .arg(Arg::new("show_htaccess").action(ArgAction::SetTrue) + .arg(Arg::new("show_htaccess") + .action(ArgAction::SetTrue) .help( "Enable display of items containing .ht when they return 403 responses") .long("show-htaccess") @@ -707,12 +743,14 @@ set to 0 to disable") .long("max-errors") .next_line_help(true) .value_parser(value_parser!(u32))) - .arg(Arg::new("no_color").action(ArgAction::SetTrue) + .arg(Arg::new("no_color") + .action(ArgAction::SetTrue) .alias("no-colour") .help("Disable coloring of terminal output") .long("no-color") .next_line_help(true)) - .arg(Arg::new("length_blacklist").action(ArgAction::Append) + .arg(Arg::new("length_blacklist") + .action(ArgAction::Append) .help( "Specify length ranges to hide, e.g. --hide-lengths 348,500-700") .long("hide-lengths") diff --git a/src/main.rs b/src/main.rs index bb579b8..2573979 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,16 +15,16 @@ // You should have received a copy of the GNU General Public License // along with Dirble. If not, see . -use log::{debug, error, info, warn, LevelFilter}; +use log::{LevelFilter, debug, error, info, warn}; use simplelog::{ColorChoice, TermLogger, TerminalMode}; use std::{ collections::VecDeque, env::current_exe, path::Path, sync::{ + Arc, atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, Sender}, - Arc, }, thread, time::Duration, diff --git a/src/wordlist.rs b/src/wordlist.rs index 416ab82..83a9def 100644 --- a/src/wordlist.rs +++ b/src/wordlist.rs @@ -17,7 +17,7 @@ use crate::validator_thread::TargetValidator; use chardet::{charset2encoding, detect}; -use encoding::{label::encoding_from_whatwg_label, DecoderTrap}; +use encoding::{DecoderTrap, label::encoding_from_whatwg_label}; use std::{fs, path::Path, sync::Arc}; use url::Url;