(feat): update from old clap definition

This commit is contained in:
Eduard Tolosa 2025-08-02 15:10:52 -05:00
parent f5049e1074
commit cba63c2a04
7 changed files with 173 additions and 243 deletions

View file

@ -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 <edu4rdshl@protonmail.com>", 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<String>,
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<String>,
/// 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<String>,
/// Remove informative messages
#[arg(short, long)]
pub quiet: bool,
/// Number of threads to use to perform the resolution
#[arg(long)]
pub threads: Option<usize>,
/// 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<String>,
/// 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<String>,
/// Nmap --min-rate value for ports scan
#[arg(long)]
pub min_rate: Option<String>,
/// 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<String>,
pub resolvers: Vec<String>,
pub targets: HashSet<String>,
pub time_wasted: Instant,
}

View file

@ -1,113 +0,0 @@
name: Unimap
author: Eduard Tolosa <edu4rdshl@protonmail.com>
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

View file

@ -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<String>) -> Vec<String> {
pub fn return_file_targets(args: &ProcessedArgs, mut files: Vec<String>) -> Vec<String> {
let mut targets: Vec<String> = 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<std::fs::File>) -> Result<
}
#[must_use]
pub fn return_output_file(args: &Args) -> Option<File> {
pub fn return_output_file(args: &ProcessedArgs) -> Option<File> {
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<String>) {
pub fn delete_files<S: ::std::hash::BuildHasher>(paths: &HashSet<String, S>) {
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<String>) {
}
}
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(())
}

View file

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

View file

@ -12,31 +12,6 @@ pub fn sanitize_target_string(target: String) -> String {
.replace('/', "")
}
pub fn return_matches_vec(matches: &clap::ArgMatches, value: &str) -> Vec<String> {
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<String> {
if matches.is_present(value) {
matches
.values_of(value)
.unwrap()
.map(str::to_owned)
.collect()
} else {
HashSet::new()
}
}
pub fn read_stdin() -> HashSet<String> {
let mut buffer = String::new();
let mut stdin = io::stdin();

View file

@ -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::<Vec<String>>()
.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<Nmaprun, serde_xml_rs::Error> {
let min_rate = min_rate.to_string();
let nmap_dns_resolvers = resolvers
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let mut nmap_args = vec![
"nmap",
"--dns-servers",
&NMAP_DNS_RESOLVERS,
&nmap_dns_resolvers,
"-Pn",
"-sS",
"--open",

View file

@ -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<String>,
pub resolvers: Vec<String>,
pub targets: HashSet<String>,
pub time_wasted: Instant,
}
use crate::nmap::Port;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ResolvData {