Compare commits

...

11 commits

Author SHA1 Message Date
RoomWithOutRoof
20b6822834
fix: correct sucessfully to successfully typo in echo message (#14) 2026-06-27 16:12:46 -05:00
c6a06db04b (chore): bump dependencies and adapt code to new ones 2025-08-02 15:11:48 -05:00
a6c166e470 (chore): remove invalid DNS 2025-08-02 15:11:15 -05:00
cba63c2a04 (feat): update from old clap definition 2025-08-02 15:10:52 -05:00
f5049e1074 Update manpage and exclude binaries folder 2025-08-02 14:05:49 -05:00
bc08bab228 Update builder.sh 2025-08-02 13:55:44 -05:00
f56b71a008 (chore): update builder.sh
- Disable docker checks, we now use podman
- Use cross for all builds, otherwise the build ends up in glibc versions mismatch
2025-08-02 13:00:43 -05:00
127d1f0a9a Update dependencies. 2025-04-20 14:47:28 -05:00
d72e115859 Clippy fixes. 2025-04-20 14:47:21 -05:00
482b6f6e21 (chore): show invalid IP address found. 2025-04-07 16:13:13 +00:00
8b0c7fcb27 (fix): do not launch nmap scans if not valid IPs are found.
Fixes #12
2025-04-07 16:10:42 +00:00
18 changed files with 2027 additions and 1191 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/logs
*.txt
/unimap_logs
/ghbinaries

2
.vscode/launch.json vendored
View file

@ -9,7 +9,7 @@
"request": "launch",
"name": "Debug",
"program": "${workspaceRoot}/target/debug/${workspaceRootFolderName}",
"args": "-f targets.txt -u log.csv --resolvers resolvers.txt --iport 1 --lport 10000 --min-rate 10",
"args": "-t useast1.sugarcrm.com --fast-scan",
"cwd": "${workspaceRoot}",
"sourceLanguages": [
"rust"

2235
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "unimap"
version = "0.6.0"
version = "0.7.0"
authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"]
edition = "2018"
description = "Scan only once by IP address and reduce scan times with Nmap for large amounts of data."
@ -12,33 +12,31 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
trust-dns-resolver = "0.20.4"
rayon = "1.7.0"
log = { version = "0.4.20", features = ["std"] }
colored = { version = "2.0.4", optional = true }
rand = "0.8.5"
lazy_static = "1.4.0"
serde = { version = "1.0.186", features = ["derive"] }
serde_derive = "1.0.186"
prettytable-rs = "0.8.0"
serde-xml-rs = "0.5.1"
chrono = "0.4.26"
clap = { version = "2.34.0", features = ["yaml"] }
config = { version = "0.11.0", features = ["yaml", "json", "toml", "hjson", "ini"] }
hickory-resolver = "0.25.2"
rayon = "1.10.0"
log = { version = "0.4.27", features = ["std"] }
colored = { version = "3.0.0", optional = true }
rand = "0.9.2"
lazy_static = "1.5.0"
tokio = { version = "1.47.0", features = ["rt"] }
futures = "0.3"
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
prettytable-rs = "0.10.0"
serde-xml-rs = "0.8.1"
chrono = "0.4.41"
clap = { version = "4.5.42", features = ["derive"] }
config = { version = "0.15.13", features = ["yaml", "json", "toml", "ini"] }
failure = "0.1.8"
# https://github.com/bluejekyll/trust-dns/pull/1632
[patch.crates-io]
trust-dns-resolver = { git = "https://github.com/Findomain/trust-dns", package = "trust-dns-resolver", branch = "custombranch" }
[target.arm-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.56", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.aarch64-unknown-linux-gnu.dependencies]
openssl = { version = "0.10.56", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.armv7-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.56", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.'cfg(windows)'.dependencies]
atty = "0.2.14"
@ -49,5 +47,4 @@ winapi = { version = "0.3.9", features = ["handleapi", "winbase"] }
lto = true
codegen-units = 1
panic = 'abort'
incremental = false
opt-level = "s"
strip = true

View file

@ -1,5 +1,4 @@
#!/usr/bin/env bash
# Rusolver releaser
NAME="unimap"
LINUX_TARGET="x86_64-unknown-linux-musl"
@ -7,24 +6,21 @@ LINUX_X86_TARGET="i686-unknown-linux-musl"
WIN_TARGET="x86_64-pc-windows-gnu"
ARMV7_TARGET="armv7-unknown-linux-gnueabihf"
AARCH_TARGET="aarch64-unknown-linux-gnu"
OSX_TARGET="x86_64-apple-darwin"
# OSX_TARGET="x86_64-apple-darwin"
MANPAGE_DIR="./$NAME.1"
BIN_OUTPUT_DIR="./ghbinaries"
if ! systemctl is-active docker >/dev/null 2>&1; then
echo "Docker is not running. Starting docker."
if ! sudo systemctl start docker; then
echo "Failed to start docker."
exit 1
fi
fi
rm -rf "$BIN_OUTPUT_DIR"
mkdir -p "$BIN_OUTPUT_DIR"
# Linux build
echo "Building Linux artifact."
if cargo build -q --release --target="$LINUX_TARGET"; then
if cross build -q --release --target="$LINUX_TARGET"; then
echo "Linux artifact build: SUCCESS"
cp "target/$LINUX_TARGET/release/$NAME" "target/$LINUX_TARGET/release/$NAME-linux"
strip "target/$LINUX_TARGET/release/$NAME-linux"
sha512sum "target/$LINUX_TARGET/release/$NAME-linux" >"target/$LINUX_TARGET/release/$NAME-linux.sha512"
sha512sum "target/$LINUX_TARGET/release/$NAME-linux" >"$BIN_OUTPUT_DIR/$NAME-linux.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-x64.zip" "target/$LINUX_TARGET/release/$NAME-linux"
else
echo "Linux artifact build: FAILED"
fi
@ -35,7 +31,8 @@ if cross build -q --release --target="$LINUX_X86_TARGET"; then
echo "Linux x86 artifact build: SUCCESS"
cp "target/$LINUX_X86_TARGET/release/$NAME" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
strip "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
sha512sum "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" >"target/$LINUX_X86_TARGET/release/$NAME-linux-i386.sha512"
sha512sum "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" >"$BIN_OUTPUT_DIR/$NAME-linux-i386.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-i386.zip" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
else
echo "Linux x86 artifact build: FAILED"
fi
@ -46,7 +43,8 @@ if cross build -q --release --target="$WIN_TARGET"; then
echo "Windows artifact build: SUCCESS"
cp "target/$WIN_TARGET/release/$NAME.exe" "target/$WIN_TARGET/release/$NAME-windows.exe"
strip "target/$WIN_TARGET/release/$NAME-windows.exe"
sha512sum "target/$WIN_TARGET/release/$NAME-windows.exe" >"target/$WIN_TARGET/release/$NAME-windows.exe.sha512"
sha512sum "target/$WIN_TARGET/release/$NAME-windows.exe" >"$BIN_OUTPUT_DIR/$NAME-windows.exe.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-windows.zip" "target/$WIN_TARGET/release/$NAME-windows.exe"
else
echo "Windows artifact build: FAILED"
fi
@ -57,7 +55,8 @@ if cross build -q --release --target="$ARMV7_TARGET"; then
echo "ARMv7 artifact build: SUCCESS"
cp "target/$ARMV7_TARGET/release/$NAME" "target/$ARMV7_TARGET/release/$NAME-armv7"
strip "target/$ARMV7_TARGET/release/$NAME-armv7"
sha512sum "target/$ARMV7_TARGET/release/$NAME-armv7" >"target/$ARMV7_TARGET/release/$NAME-armv7.sha512"
sha512sum "target/$ARMV7_TARGET/release/$NAME-armv7" >"$BIN_OUTPUT_DIR/$NAME-armv7.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-armv7.zip" "target/$ARMV7_TARGET/release/$NAME-armv7"
else
echo "ARMv7 artifact build: FAILED"
fi
@ -68,41 +67,31 @@ if cross build -q --release --target="$AARCH_TARGET"; then
echo "Aarch64 artifact build: SUCCESS"
cp "target/$AARCH_TARGET/release/$NAME" "target/$AARCH_TARGET/release/$NAME-aarch64"
strip "target/$AARCH_TARGET/release/$NAME-aarch64"
sha512sum "target/$AARCH_TARGET/release/$NAME-aarch64" >"$BIN_OUTPUT_DIR/$NAME-aarch64.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-aarch64.zip" "target/$AARCH_TARGET/release/$NAME-aarch64"
else
echo "Aarch64 artifact build: FAILED"
fi
# Mac OS build
echo "Building OSX artifact."
if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then
echo "OSX artifact build: SUCCESS"
cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx"
strip "target/$OSX_TARGET/release/$NAME-osx"
sha512sum "target/$OSX_TARGET/release/$NAME-osx" >"target/$OSX_TARGET/release/$NAME-osx.sha512"
else
echo "OSX artifact build: FAILED"
fi
# # Mac OS build
# echo "Building OSX artifact."
# if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then
# echo "OSX artifact build: SUCCESS"
# cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx"
# strip "target/$OSX_TARGET/release/$NAME-osx"
# sha512sum "target/$OSX_TARGET/release/$NAME-osx" >"target/$OSX_TARGET/release/$NAME-osx.sha512"
# zip -q "$BIN_OUTPUT_DIR/$NAME-osx.zip" "target/$OSX_TARGET/release/$NAME-osx"
# else
# echo "OSX artifact build: FAILED"
# fi
echo "Creating manpage..."
if command -v help2man >/dev/null; then
if help2man -o "$MANPAGE_DIR" "target/$LINUX_TARGET/release/$NAME"; then
echo "Manpage created sucessfully and saved in $MANPAGE_DIR"
echo "Manpage created successfully and saved in $MANPAGE_DIR"
else
echo "Error creating manpage."
fi
else
echo "Please install the help2man package."
fi
if command -v git >/dev/null; then
git add .
git commit -m "Bump version."
git push
fi
# Stop docker
echo "Stopping docker."
if ! sudo systemctl stop docker; then
echo "Failed to stop docker."
exit 1
fi

View file

@ -1,72 +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)]
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()
},
};
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

@ -16,12 +16,6 @@ pub fn ipv4_resolvers() -> Vec<String> {
// Verisign
"64.6.64.6",
"64.6.65.6",
// UncensoredDNS
"91.239.100.100",
"89.233.43.71",
// dns.watch
"84.200.69.80",
"84.200.70.40",
]
.into_iter()
.map(str::to_owned)

View file

@ -1,5 +1,8 @@
use {
crate::{errors::*, structs::Args},
crate::{
args::ProcessedArgs,
errors::{Result, ResultExt},
},
log::error,
prettytable::Table,
std::{
@ -10,26 +13,27 @@ use {
},
};
pub fn return_file_targets(args: &Args, mut files: Vec<String>) -> Vec<String> {
#[must_use]
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);
}
}
Err(e) => {
if args.files.len() == 1 {
error!("Can not open file {}. Error: {}\n", f, e);
error!("Can not open file {f}. Error: {e}\n");
std::process::exit(1)
} else if !args.quiet_flag {
error!(
"Can not open file {}, working with next file. Error: {}\n",
f, e
);
error!("Can not open file {f}, working with next file. Error: {e}\n");
}
}
}
@ -44,7 +48,8 @@ pub fn table_to_file(table: &Table, file_name: Option<std::fs::File>) -> Result<
Ok(())
}
pub fn return_output_file(args: &Args) -> Option<File> {
#[must_use]
pub fn return_output_file(args: &ProcessedArgs) -> Option<File> {
if args.file_name.is_empty() || !args.with_output {
None
} else {
@ -59,23 +64,24 @@ pub fn return_output_file(args: &Args) -> Option<File> {
}
}
#[must_use]
pub fn check_full_path(full_path: &str) -> bool {
(Path::new(full_path).exists() && Path::new(full_path).is_dir())
|| 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) {
Ok(_) => (),
Ok(()) => (),
Err(e) => error!("Error deleting the file {}. Description: {}", &file, e),
}
}
}
}
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,6 +1,5 @@
#[cfg(windows)]
extern crate atty;
#[cfg(feature = "chrono")]
extern crate chrono;
#[cfg(feature = "colored")]
extern crate colored;
@ -8,7 +7,6 @@ extern crate log;
#[cfg(windows)]
extern crate winapi;
#[cfg(feature = "chrono")]
use chrono::Local;
#[cfg(feature = "colored")]
use colored::*;
@ -23,10 +21,10 @@ impl Log for SimpleLogger {
}
fn log(&self, record: &Record) {
let target = if !record.target().is_empty() {
record.target()
} else {
let target = if record.target().is_empty() {
record.module_path().unwrap_or_default()
} else {
record.target()
};
if target.contains("unimap") && self.enabled(record.metadata()) {
let level_string = {
@ -45,7 +43,6 @@ impl Log for SimpleLogger {
record.level().to_string()
}
};
#[cfg(feature = "chrono")]
{
print!(
"\n{} [{}] {}",
@ -54,10 +51,6 @@ impl Log for SimpleLogger {
record.args()
);
}
#[cfg(not(feature = "chrono"))]
{
print!("\n[{}] {}", level_string, record.args());
}
}
}

View file

@ -9,7 +9,7 @@ pub fn validate_target(target: &str) -> bool {
!target.starts_with('.')
&& target.contains('.')
&& !target.contains(&SPECIAL_CHARS[..])
&& target.chars().all(|c| c.is_ascii())
&& target.is_ascii()
}
pub fn null_ip_checker(ip: &str) -> String {

View file

@ -1,27 +1,30 @@
use {
clap::Parser,
log::{error, Level},
std::{collections::HashSet, iter::FromIterator},
unimap::{args, errors::*, files::return_file_targets, logger, misc, resolver_engine},
unimap::{args, errors::Result, files::return_file_targets, logger, misc, resolver_engine},
};
fn run() -> Result<()> {
if std::env::var("UNIMAP_LOG_LEVEL").is_ok() {
logger::init_by_env()
logger::init_by_env();
} else {
logger::init_with_level(Level::Info).unwrap()
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()))
HashSet::from_iter(return_file_targets(&arguments, arguments.files.clone()));
} else if !arguments.target.is_empty() {
arguments.targets.insert(arguments.target.clone());
} else {
arguments.targets = misc::read_stdin()
arguments.targets = misc::read_stdin();
}
if arguments.targets.len() < 50 {
arguments.threads = arguments.targets.len()
arguments.threads = arguments.targets.len();
}
rayon::ThreadPoolBuilder::new()
@ -29,19 +32,19 @@ fn run() -> Result<()> {
.build_global()
.unwrap();
if !arguments.targets.is_empty() {
resolver_engine::parallel_resolver_all(&mut arguments)
} else {
if arguments.targets.is_empty() {
error!("Error: Target is empty or invalid!\n");
std::process::exit(1)
} else {
resolver_engine::parallel_resolver_all(&mut arguments)
}
}
fn main() {
if let Err(err) = run() {
error!("Error: {}", err);
error!("Error: {err}");
for cause in err.iter_chain().skip(1) {
error!("Error description: {}\n", cause);
error!("Error description: {cause}\n");
}
std::process::exit(1);
}

View file

@ -3,6 +3,7 @@ use std::{
io::{self, Read},
};
#[must_use]
pub fn sanitize_target_string(target: String) -> String {
target
.replace("www.", "")
@ -11,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,43 +1,43 @@
use {
crate::{files, structs::Args},
std::{collections::HashSet, net::SocketAddr},
trust_dns_resolver::{
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts},
Resolver,
crate::{args::ProcessedArgs, files},
hickory_resolver::{
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
name_server::TokioConnectionProvider,
proto::xfer::Protocol,
TokioResolver,
},
std::{collections::HashSet, net::SocketAddr},
};
pub fn get_records(resolver: &Resolver, domain: &str) -> String {
if let Ok(ips) = resolver.ipv4_lookup(domain) {
pub fn get_records(resolver: &TokioResolver, domain: &str) -> String {
futures::executor::block_on(resolver.ipv4_lookup(domain)).map_or_else(
|_| String::new(),
|ips| {
ips.iter()
.map(|x| x.to_string())
.map(std::string::ToString::to_string)
.next()
.expect("Failed to get IPV4.")
} else {
String::new()
}
},
)
}
pub fn get_resolver(nameserver_ips: HashSet<SocketAddr>, opts: ResolverOpts) -> Resolver {
let mut name_servers = NameServerConfigGroup::with_capacity(nameserver_ips.len() * 2);
name_servers.extend(nameserver_ips.into_iter().flat_map(|socket_addr| {
std::iter::once(NameServerConfig {
socket_addr,
protocol: Protocol::Udp,
tls_dns_name: None,
trust_nx_responses: false,
})
.chain(std::iter::once(NameServerConfig {
socket_addr,
protocol: Protocol::Tcp,
tls_dns_name: None,
trust_nx_responses: false,
}))
}));
Resolver::new(ResolverConfig::from_parts(None, vec![], name_servers), opts).unwrap()
pub fn get_resolver(nameserver_ips: HashSet<SocketAddr>, opts: ResolverOpts) -> TokioResolver {
let mut name_servers = NameServerConfigGroup::with_capacity(nameserver_ips.len());
name_servers.extend(
nameserver_ips
.into_iter()
.map(|socket_addr| NameServerConfig::new(socket_addr, Protocol::Udp)),
);
TokioResolver::builder_with_config(
ResolverConfig::from_parts(None, vec![], name_servers),
TokioConnectionProvider::default(),
)
.with_options(opts)
.build()
}
pub fn return_socket_address(args: &Args) -> HashSet<SocketAddr> {
pub fn return_socket_address(args: &ProcessedArgs) -> HashSet<SocketAddr> {
let mut resolver_ips = HashSet::new();
if args.custom_resolvers {
for r in &files::return_file_targets(args, args.resolvers.clone()) {

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",
@ -132,17 +129,17 @@ pub fn get_nmap_data(
];
if !min_rate.is_empty() {
nmap_args.append(&mut vec!["--min-rate", &min_rate])
nmap_args.append(&mut vec!["--min-rate", &min_rate]);
}
if fast_scan {
nmap_args.append(&mut vec!["--host-timeout", "20m"])
nmap_args.append(&mut vec!["--host-timeout", "20m"]);
} else {
nmap_args.append(&mut vec!["-sV"])
nmap_args.append(&mut vec!["-sV"]);
}
if !ports.is_empty() {
nmap_args.append(&mut vec!["-p", ports])
nmap_args.append(&mut vec!["-p", ports]);
}
nmap_args.push(host);
@ -159,8 +156,7 @@ pub fn get_nmap_data(
}
Err(e) => {
error!(
"Error waiting command to finish for {}, continuing with remaining hosts. Description: {}",
host, e
"Error waiting command to finish for {host}, continuing with remaining hosts. Description: {e}"
);
Ok(Nmaprun::default())
}

View file

@ -1,11 +1,12 @@
use {
crate::{
args,
errors::*,
args::ProcessedArgs,
errors::Result,
files, logic, networking,
nmap::{self, Nmaprun},
structs::{Args, ResolvData},
structs::ResolvData,
},
hickory_resolver::config::{LookupIpStrategy, ResolverOpts},
log::{error, info},
prettytable,
prettytable::Table,
@ -15,61 +16,55 @@ use {
net::Ipv4Addr,
time::Duration,
},
trust_dns_resolver::{
config::{LookupIpStrategy, ResolverOpts},
Resolver,
},
};
lazy_static! {
pub static ref RESOLVERS: Vec<Ipv4Addr> = {
let args = args::get_args();
fn create_resolvers(args: &ProcessedArgs) -> Vec<Ipv4Addr> {
let mut resolver_ips = Vec::new();
if args.custom_resolvers {
for r in &files::return_file_targets(&args, args.resolvers.clone()) {
for r in &files::return_file_targets(args, args.resolvers.clone()) {
match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip),
Err(e) => {
error!("Error parsing the {} IP from resolvers file to IP address. Please check and try again. Error: {}\n", r, e);
error!("Error parsing the {r} IP from resolvers file to IP address. Please check and try again. Error: {e}\n");
std::process::exit(1)
}
}
}
} else {
for r in args.resolvers {
for r in &args.resolvers {
match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip),
Err(e) => {
error!("Error parsing the {} IP from resolvers file to IP address. Please check and try again. Error: {}\n", r, e);
error!("Error parsing the {r} IP from resolvers file to IP address. Please check and try again. Error: {e}\n");
std::process::exit(1)
}
}
}
}
resolver_ips
};
}
pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
files::check_full_path(&args.logs_dir);
pub fn parallel_resolver_all(args: &mut ProcessedArgs) -> Result<()> {
if !files::check_full_path(&args.logs_dir) {
error!("The logs directory {} does not exist.\n", args.logs_dir);
std::process::exit(1)
}
if !args.quiet_flag {
info!(
"Performing parallel resolution for {} targets with {} threads, it will take a while...\n",
args.targets.len(), args.threads
)
);
}
let opts = ResolverOpts {
timeout: Duration::from_secs(1),
ip_strategy: LookupIpStrategy::Ipv4Only,
num_concurrent_reqs: 1,
..Default::default()
};
let mut opts = ResolverOpts::default();
opts.timeout = Duration::from_secs(1);
opts.ip_strategy = LookupIpStrategy::Ipv4Only;
opts.num_concurrent_reqs = 1;
let resolver = networking::get_resolver(networking::return_socket_address(args), opts);
let data = parallel_resolver_engine(args, args.targets.clone(), resolver);
let data = parallel_resolver_engine(args, &args.targets, &resolver);
let mut table = Table::new();
table.set_titles(row![
@ -79,9 +74,9 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
"SERVICES"
]);
if args.raw_output && !args.quiet_flag {
println!("HOST,IP,PORT,SERVICE,VERSION,PRODUCT,OS,EXTRAINFO")
println!("HOST,IP,PORT,SERVICE,VERSION,PRODUCT,OS,EXTRAINFO");
} else if args.url_output && !args.quiet_flag {
println!("HOST:IP")
println!("HOST:IP");
}
for (target, resolv_data) in &data {
if !resolv_data.ip.is_empty() {
@ -120,11 +115,11 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
.extrainfo
.clone()
.unwrap_or_else(|| "NULL".to_string())
)
);
}
} else if args.url_output {
for port_data in &resolv_data.ports_data {
println!("{}:{}", target, port_data.portid)
println!("{}:{}", target, port_data.portid);
}
} else {
let mut services_table = Table::new();
@ -179,7 +174,7 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
error!(
"An error occurred while writing the output file {}.\n",
args.file_name
)
);
}
if !args.quiet_flag && !args.raw_output && !args.url_output {
table.printstd();
@ -199,36 +194,51 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
}
fn parallel_resolver_engine(
args: &Args,
targets: HashSet<String>,
resolver: Resolver,
args: &ProcessedArgs,
targets: &HashSet<String>,
resolver: &hickory_resolver::TokioResolver,
) -> HashMap<String, ResolvData> {
let resolv_data: HashMap<String, ResolvData> = targets
.par_iter()
.map(|target| {
let fqdn_target = format!("{}.", target);
let fqdn_target = format!("{target}.");
let mut resolv_data = ResolvData::default();
resolv_data.ip = networking::get_records(&resolver, &fqdn_target);
resolv_data.ip = networking::get_records(resolver, &fqdn_target);
(target.to_owned(), resolv_data)
})
.collect();
let resolvers = create_resolvers(args);
let mut nmap_ips: HashSet<String> = resolv_data
.values()
.map(|resolv_data| resolv_data.ip.clone())
.collect();
let nmap_ips_orig = nmap_ips.clone();
nmap_ips.retain(|ip| {
!ip.is_empty()
&& ip.parse::<Ipv4Addr>().is_ok()
&& !ip.parse::<Ipv4Addr>().unwrap().is_private()
&& ip.parse::<Ipv4Addr>().is_ok()
});
if nmap_ips.is_empty() {
error!("No valid IPs found for scanning. IPs found: {nmap_ips_orig:?}\n");
std::process::exit(1)
} else {
let nmap_data: HashMap<String, Nmaprun> = nmap_ips
.par_iter()
.map(|ip| {
let filename = format!("{}/{}.xml", &args.logs_dir, &ip);
match nmap::get_nmap_data(&filename, ip, &args.min_rate, &args.ports, args.fast_scan) {
match nmap::get_nmap_data(
&filename,
ip,
&args.min_rate,
&args.ports,
args.fast_scan,
&resolvers,
) {
Ok(nmap_data) => {
nmap_data
.host
@ -239,7 +249,7 @@ fn parallel_resolver_engine(
.port
.retain(|f| f.state.state == "open");
if args.no_keep_nmap_logs && std::fs::remove_file(&filename).is_err() {
error!("Error removing filename {}.", &filename)
error!("Error removing filename {}.", &filename);
}
(ip.clone(), nmap_data)
}
@ -253,7 +263,7 @@ fn parallel_resolver_engine(
// Delete the args.logs_dir directory if it's empty
if args.no_keep_nmap_logs && std::fs::remove_dir(&args.logs_dir).is_err() {
error!("Error removing directory {}.", &args.logs_dir)
error!("Error removing directory {}.", &args.logs_dir);
}
resolv_data
@ -282,3 +292,4 @@ fn parallel_resolver_engine(
})
.collect()
}
}

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 {
@ -35,8 +6,8 @@ pub struct ResolvData {
pub ports_data: Vec<Port>,
}
impl ResolvData {
pub fn default() -> ResolvData {
ResolvData {
pub const fn default() -> Self {
Self {
ip: String::new(),
ports_data: Vec::new(),
}

106
unimap.1
View file

@ -1,81 +1,73 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.3.
.TH UNIMAP "1" "May 2021" "Unimap 0.5.1" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH UNIMAP "1" "August 2025" "unimap 0.7.0" "User Commands"
.SH NAME
Unimap \- manual page for Unimap 0.5.1
unimap \- manual page for unimap 0.7.0
.SH SYNOPSIS
.B unimap
[\fI\,OPTIONS\/\fR]
.SH DESCRIPTION
Unimap 0.5.1
Eduard Tolosa <edu4rdshl@protonmail.com>
Scan only once by IP address and reduce scan times with Nmap for large amounts of data.
.SS "USAGE:"
.IP
unimap [FLAGS] [OPTIONS]
.SS "FLAGS:"
.SH OPTIONS
.TP
\fB\-\-fast\-scan\fR
Use fast scanning for ports (no version detection).
\fB\-t\fR, \fB\-\-target\fR <TARGET>
Target host
.TP
\fB\-h\fR, \fB\-\-help\fR
Prints help information
.TP
\fB\-k\fR, \fB\-\-no\-keep\-nmap\-logs\fR
Keep Nmap XML files created in the logs/ folder for every scanned IP. This data will be
useful for other tasks.
\fB\-f\fR, \fB\-\-files\fR <FILES>
Use a list of targets written in a file as input
.TP
\fB\-o\fR, \fB\-\-output\fR
Write to an output file. The name of the output file will be unimap\-log\-date.
Write to an output file. The name of the output file will be unimap\-log\-date
.TP
\fB\-u\fR, \fB\-\-unique\-output\fR <UNIQUE_OUTPUT>
Write the output in CSV format to the specified filename
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Remove informative messages.
Remove informative messages
.TP
\fB\-\-threads\fR <THREADS>
Number of threads to use to perform the resolution
.TP
\fB\-\-resolvers\fR <CUSTOM_RESOLVERS>
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
.TP
\fB\-\-ports\fR <PORTS>
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"
.TP
\fB\-\-min\-rate\fR <MIN_RATE>
Nmap \fB\-\-min\-rate\fR value for ports scan
.TP
\fB\-\-fast\-scan\fR
Use fast scanning for ports (no version detection)
.TP
\fB\-\-logs\-dir\fR <LOGS_DIR>
Path to save the CSV data of the process and/or Nmap XML files. Default to logs/ [default: unimap_logs]
.TP
\fB\-k\fR, \fB\-\-no\-keep\-nmap\-logs\fR
Keep Nmap XML files created in the logs/ folder for every scanned IP. This data will be useful for other tasks
.TP
\fB\-r\fR, \fB\-\-raw\-output\fR
Use raw output instead of a table.
.TP
\fB\-\-stdin\fR
Read from stdin instead of files or aguments.
Use raw output instead of a table
.TP
\fB\-\-url\-output\fR
Use HOST:IP output format.
Use HOST:IP output format
.TP
\fB\-\-stdin\fR
Read from stdin instead of files or arguments
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
\fB\-V\fR, \fB\-\-version\fR
Prints version information
.SS "OPTIONS:"
.TP
\fB\-\-resolvers\fR <custom\-resolvers>...
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.
.TP
\fB\-f\fR, \fB\-\-file\fR <files>...
Use a list of targets writen in a file as input.
.TP
\fB\-\-logs\-dir\fR <logs\-dir>
Path to save the CSV data of the process and/or Nmap XML files. Default to
logs/.
.TP
\fB\-\-min\-rate\fR <min\-rate>
Nmap \fB\-\-min\-rate\fR value for ports scan.
.TP
\fB\-\-ports\fR <ports>
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"
.TP
\fB\-t\fR, \fB\-\-target\fR <target>
Target host.
.TP
\fB\-\-threads\fR <threads>
Number of threads to use to perform the resolution.
.TP
\fB\-u\fR, \fB\-\-unique\-output\fR <unique\-output>
Write the output in CSV format to the specified filename.
Print version
.SH "SEE ALSO"
The full documentation for
.B Unimap
.B unimap
is maintained as a Texinfo manual. If the
.B info
and
.B Unimap
.B unimap
programs are properly installed at your site, the command
.IP
.B info Unimap
.B info unimap
.PP
should give you access to the complete manual.