diff --git a/src/args.rs b/src/args.rs index d3f6cc5..91a6da7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,73 +1,167 @@ use { - crate::{ - defaults, - logic::validate_target, - misc::{return_matches_vec, sanitize_target_string}, - structs::Args, - }, + crate::{defaults, logic::validate_target, misc::sanitize_target_string}, chrono::Utc, - clap::{load_yaml, value_t, App}, + clap::Parser, std::{collections::HashSet, time::Instant}, }; -#[allow(clippy::cognitive_complexity)] -#[must_use] -pub fn get_args() -> Args { - let yaml = load_yaml!("cli.yml"); - let matches = App::from_yaml(yaml) - .version(clap::crate_version!()) - .get_matches(); +/// Scan only once by IP address and reduce scan times with Nmap for large amounts of data. +#[derive(Parser, Debug, Clone)] +#[command(author = "Eduard Tolosa ", version, about, long_about = None)] +#[command(arg_required_else_help = true)] +pub struct Args { + /// Target host + #[arg(short, long, conflicts_with_all = ["files", "stdin"])] + pub target: Option, - Args { - target: { - let target = sanitize_target_string( - value_t!(matches, "target", String).unwrap_or_else(|_| String::new()), - ); - if validate_target(&target) { - target + /// Use a list of targets written in a file as input + #[arg(short, long, conflicts_with_all = ["target", "stdin"])] + pub files: Vec, + + /// Write to an output file. The name of the output file will be unimap-log-date + #[arg(short, long, conflicts_with = "unique_output")] + pub output: bool, + + /// Write the output in CSV format to the specified filename + #[arg(short, long, conflicts_with = "output")] + pub unique_output: Option, + + /// Remove informative messages + #[arg(short, long)] + pub quiet: bool, + + /// Number of threads to use to perform the resolution + #[arg(long)] + pub threads: Option, + + /// Path to a file (or files) containing a list of DNS IP address. If no specified then a list of built-in DNS servers is used + #[arg(long = "resolvers")] + pub custom_resolvers: Vec, + + /// Ports to scan. You can specify a range of ports, a list, or both. Put them inside double quotes, for example: "22, 80, 443, 1000-5000" + #[arg(long)] + pub ports: Option, + + /// Nmap --min-rate value for ports scan + #[arg(long)] + pub min_rate: Option, + + /// Use fast scanning for ports (no version detection) + #[arg(long)] + pub fast_scan: bool, + + /// Path to save the CSV data of the process and/or Nmap XML files. Default to logs/ + #[arg(long, default_value = "unimap_logs")] + pub logs_dir: String, + + /// Keep Nmap XML files created in the logs/ folder for every scanned IP. This data will be useful for other tasks + #[arg(short = 'k', long)] + pub no_keep_nmap_logs: bool, + + /// Use raw output instead of a table + #[arg(short, long, conflicts_with = "url_output")] + pub raw_output: bool, + + /// Use HOST:IP output format + #[arg(long, conflicts_with = "raw_output")] + pub url_output: bool, + + /// Read from stdin instead of files or arguments + #[arg(long, conflicts_with_all = ["files", "target"])] + pub stdin: bool, +} + +impl Args { + /// Create the processed args with computed fields + #[must_use] + pub fn into_processed_args(self) -> ProcessedArgs { + let target = self.target.map_or_else(String::new, |target| { + let sanitized = sanitize_target_string(target); + if validate_target(&sanitized) { + sanitized } else { String::new() } - }, - file_name: if matches.is_present("output") { - value_t!(matches, "logs-dir", String).unwrap_or_else(|_| "unimap_logs".to_string()) - + "/" - + "unimap" - + &Utc::now().format("-log-%Y-%m-%d_%H-%M-%S").to_string() - + ".csv" - } else if matches.is_present("unique-output") { - matches.value_of("unique-output").unwrap().to_string() + }); + + let file_name = if self.output { + format!( + "{}/unimap{}", + self.logs_dir, + Utc::now().format("-log-%Y-%m-%d_%H-%M-%S") + ) + ".csv" + } else if let Some(unique_output) = &self.unique_output { + unique_output.clone() } else { String::new() - }, - logs_dir: value_t!(matches, "logs-dir", String) - .unwrap_or_else(|_| "unimap_logs".to_string()), - threads: if matches.is_present("ports") && !matches.is_present("threads") { + }; + + let threads = if self.ports.is_some() && self.threads.is_none() { 30 } else { - value_t!(matches, "threads", usize).unwrap_or_else(|_| 50) - }, - version: clap::crate_version!().to_string(), - ports: value_t!(matches, "ports", String).unwrap_or_else(|_| String::new()), - with_output: matches.is_present("output") || matches.is_present("unique-output"), - unique_output_flag: matches.is_present("unique-output"), - from_file_flag: matches.is_present("files"), - quiet_flag: matches.is_present("quiet"), - custom_resolvers: matches.is_present("custom-resolvers"), - custom_ports_range: matches.is_present("ports"), - fast_scan: matches.is_present("fast-scan"), - no_keep_nmap_logs: matches.is_present("no-keep-nmap-logs"), - raw_output: matches.is_present("raw-output"), - url_output: matches.is_present("url-output"), - from_stdin: matches.is_present("stdin"), - files: return_matches_vec(&matches, "files"), - min_rate: value_t!(matches, "min-rate", String).unwrap_or_else(|_| String::new()), - resolvers: if matches.is_present("custom-resolvers") { - return_matches_vec(&matches, "custom-resolvers") + self.threads.unwrap_or(50) + }; + + let custom_resolvers_flag = !self.custom_resolvers.is_empty(); + let custom_ports_range = self.ports.is_some(); + let ports_value = self.ports.unwrap_or_default(); + + let resolvers = if custom_resolvers_flag { + self.custom_resolvers } else { defaults::ipv4_resolvers() - }, - targets: HashSet::new(), - time_wasted: Instant::now(), + }; + + ProcessedArgs { + target, + file_name, + version: env!("CARGO_PKG_VERSION").to_string(), + logs_dir: self.logs_dir, + threads, + ports: ports_value, + with_output: self.output || self.unique_output.is_some(), + unique_output_flag: self.unique_output.is_some(), + min_rate: self.min_rate.unwrap_or_default(), + from_file_flag: !self.files.is_empty(), + quiet_flag: self.quiet, + custom_resolvers: custom_resolvers_flag, + custom_ports_range, + no_keep_nmap_logs: self.no_keep_nmap_logs, + raw_output: self.raw_output, + fast_scan: self.fast_scan, + url_output: self.url_output, + from_stdin: self.stdin, + files: self.files, + resolvers, + targets: HashSet::new(), + time_wasted: Instant::now(), + } } } + +/// Processed args with computed fields and flags +#[derive(Clone, Debug)] +pub struct ProcessedArgs { + pub target: String, + pub file_name: String, + pub version: String, + pub logs_dir: String, + pub threads: usize, + pub ports: String, + pub with_output: bool, + pub unique_output_flag: bool, + pub min_rate: String, + pub from_file_flag: bool, + pub quiet_flag: bool, + pub custom_resolvers: bool, + pub custom_ports_range: bool, + pub no_keep_nmap_logs: bool, + pub raw_output: bool, + pub fast_scan: bool, + pub url_output: bool, + pub from_stdin: bool, + pub files: Vec, + pub resolvers: Vec, + pub targets: HashSet, + pub time_wasted: Instant, +} diff --git a/src/cli.yml b/src/cli.yml deleted file mode 100644 index ca109bd..0000000 --- a/src/cli.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Unimap -author: Eduard Tolosa -settings: - - ArgRequiredElseHelp - - StrictUtf8 -about: Scan only once by IP address and reduce scan times with Nmap for large amounts of data. -args: - - target: - short: t - long: target - help: Target host. - takes_value: true - multiple: false - conflicts_with: - - files - - stdin - - - files: - short: f - long: file - help: Use a list of targets writen in a file as input. - takes_value: true - multiple: true - conflicts_with: - - target - - stdin - - - output: - short: o - long: output - help: Write to an output file. The name of the output file will be unimap-log-date. - takes_value: false - - - unique-output: - short: u - long: unique-output - help: Write the output in CSV format to the specified filename. - takes_value: true - multiple: false - conflicts_with: - - output - - - quiet: - short: q - long: quiet - help: Remove informative messages. - takes_value: false - - - threads: - help: Number of threads to use to perform the resolution. - long: threads - takes_value: true - - - custom-resolvers: - help: Path to a file (or files) containing a list of DNS IP address. If no specified then 1.6k of built-in valid DNS servers from public-dns.info are used. - long: resolvers - takes_value: true - multiple: true - - - ports: - help: 'Ports to scan. You can specify a range of ports, a list, or both. Put them inside double quotes, for example: "22, 80, 443, 1000-5000"' - long: ports - takes_value: true - multiple: false - - - min-rate: - help: Nmap --min-rate value for ports scan. - long: min-rate - takes_value: true - - - fast-scan: - help: Use fast scanning for ports (no version detection). - long: fast-scan - takes_value: false - - - logs-dir: - help: Path to save the CSV data of the process and/or Nmap XML files. Default to logs/. - long: logs-dir - takes_value: true - multiple: false - - - no-keep-nmap-logs: - help: Keep Nmap XML files created in the logs/ folder for every scanned IP. This data will be useful for other tasks. - short: k - long: no-keep-nmap-logs - takes_value: false - multiple: false - - - raw-output: - help: Use raw output instead of a table. - short: r - long: raw-output - takes_value: false - multiple: false - conflicts_with: - - url-output - - - url-output: - help: Use HOST:IP output format. - long: url-output - takes_value: false - multiple: false - conflicts_with: - - raw-output - - - stdin: - help: Read from stdin instead of files or aguments. - long: stdin - takes_value: false - multiple: false - conflicts_with: - - files - - target diff --git a/src/files.rs b/src/files.rs index 4678146..3564dc1 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,7 +1,7 @@ use { crate::{ + args::ProcessedArgs, errors::{Result, ResultExt}, - structs::Args, }, log::error, prettytable::Table, @@ -14,14 +14,17 @@ use { }; #[must_use] -pub fn return_file_targets(args: &Args, mut files: Vec) -> Vec { +pub fn return_file_targets(args: &ProcessedArgs, mut files: Vec) -> Vec { let mut targets: Vec = Vec::new(); files.sort(); files.dedup(); for f in files { match File::open(&f) { Ok(file) => { - for target in BufReader::new(file).lines().flatten() { + for target in BufReader::new(file) + .lines() + .map_while(std::result::Result::ok) + { targets.push(target); } } @@ -46,7 +49,7 @@ pub fn table_to_file(table: &Table, file_name: Option) -> Result< } #[must_use] -pub fn return_output_file(args: &Args) -> Option { +pub fn return_output_file(args: &ProcessedArgs) -> Option { if args.file_name.is_empty() || !args.with_output { None } else { @@ -67,7 +70,7 @@ pub fn check_full_path(full_path: &str) -> bool { || fs::create_dir_all(full_path).is_ok() } -pub fn delete_files(paths: &HashSet) { +pub fn delete_files(paths: &HashSet) { for file in paths { if Path::new(&file).exists() { match std::fs::remove_file(file) { @@ -78,7 +81,7 @@ pub fn delete_files(paths: &HashSet) { } } -pub fn string_to_file(data: String, mut file: File) -> Result<()> { +pub fn string_to_file(data: &str, mut file: File) -> Result<()> { file.write_all(data.as_bytes())?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index f2b778d..4398a91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use { + clap::Parser, log::{error, Level}, std::{collections::HashSet, iter::FromIterator}, unimap::{args, errors::Result, files::return_file_targets, logger, misc, resolver_engine}, @@ -10,7 +11,9 @@ fn run() -> Result<()> { } else { logger::init_with_level(Level::Info).unwrap(); } - let mut arguments = args::get_args(); + + let mut arguments = args::Args::parse().into_processed_args(); + if !arguments.files.is_empty() { arguments.targets = HashSet::from_iter(return_file_targets(&arguments, arguments.files.clone())); diff --git a/src/misc.rs b/src/misc.rs index adb5326..5c789d5 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -12,31 +12,6 @@ pub fn sanitize_target_string(target: String) -> String { .replace('/', "") } -pub fn return_matches_vec(matches: &clap::ArgMatches, value: &str) -> Vec { - if matches.is_present(value) { - matches - .values_of(value) - .unwrap() - .map(str::to_owned) - .collect() - } else { - Vec::new() - } -} - -#[allow(dead_code)] -pub fn return_matches_hashset(matches: &clap::ArgMatches, value: &str) -> HashSet { - if matches.is_present(value) { - matches - .values_of(value) - .unwrap() - .map(str::to_owned) - .collect() - } else { - HashSet::new() - } -} - pub fn read_stdin() -> HashSet { let mut buffer = String::new(); let mut stdin = io::stdin(); diff --git a/src/nmap.rs b/src/nmap.rs index 2192a35..a148cf0 100644 --- a/src/nmap.rs +++ b/src/nmap.rs @@ -1,18 +1,8 @@ use { - crate::resolver_engine, log::error, - std::{path::Path, process::Command}, + std::{net::Ipv4Addr, path::Path, process::Command}, }; -lazy_static! { - static ref NMAP_DNS_RESOLVERS: String = resolver_engine::RESOLVERS - .clone() - .iter() - .map(ToString::to_string) - .collect::>() - .join(","); -} - #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Root { @@ -114,12 +104,19 @@ pub fn get_nmap_data( min_rate: &str, ports: &str, fast_scan: bool, + resolvers: &[Ipv4Addr], ) -> Result { let min_rate = min_rate.to_string(); + let nmap_dns_resolvers = resolvers + .iter() + .map(ToString::to_string) + .collect::>() + .join(","); + let mut nmap_args = vec![ "nmap", "--dns-servers", - &NMAP_DNS_RESOLVERS, + &nmap_dns_resolvers, "-Pn", "-sS", "--open", diff --git a/src/structs.rs b/src/structs.rs index 5cd7162..46db1a5 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,33 +1,4 @@ -use { - crate::nmap::Port, - std::{collections::HashSet, time::Instant}, -}; - -#[derive(Clone, Debug)] -pub struct Args { - pub target: String, - pub file_name: String, - pub version: String, - pub logs_dir: String, - pub threads: usize, - pub ports: String, - pub with_output: bool, - pub unique_output_flag: bool, - pub min_rate: String, - pub from_file_flag: bool, - pub quiet_flag: bool, - pub custom_resolvers: bool, - pub custom_ports_range: bool, - pub no_keep_nmap_logs: bool, - pub raw_output: bool, - pub fast_scan: bool, - pub url_output: bool, - pub from_stdin: bool, - pub files: Vec, - pub resolvers: Vec, - pub targets: HashSet, - pub time_wasted: Instant, -} +use crate::nmap::Port; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct ResolvData {