Compare commits

...

44 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
3afaf475d0 Bump version and dependencies. 2023-08-25 00:51:41 -05:00
7c6e717c0b Remove logs_dir when the scan is finished.
Fixes #11
2023-08-23 20:35:37 -05:00
Eduard Tolosa
36419c3da7 Don't scan private IP addresses. 2023-04-11 23:18:13 -05:00
Eduard Tolosa
cde33efba1
Clippy fixes. 2023-02-09 18:40:54 -05:00
Eduard Tolosa
3c8661bc24
Use ToString trait. 2022-06-23 00:34:54 -05:00
Eduard Tolosa
97c5b458d1
Fix typo. 2022-04-16 20:27:06 -05:00
Eduard Tolosa
7ae2558dca
Add Dockerhub information.
Complements #9
2022-04-16 20:26:42 -05:00
Eduard Tolosa
aafd31ffeb
Update cicd-to-dockerhub.yml 2022-04-16 20:15:46 -05:00
Eduard Tolosa
90e6cbf8bc
Create cicd-to-dockerhub.yml 2022-04-16 20:14:34 -05:00
Rudra Sarkar
284d593207
Docker support for unimap. 2022-04-16 20:08:32 -05:00
Rudra Sarkar
0b9db85a71
Update installation instructions for global use on Linux and Mac OS 2022-04-15 03:23:12 -05:00
Eduard Tolosa
ddd486a11c
Create make-release.yml 2022-04-12 05:47:28 -05:00
Edu4rdSHL
bb030e266b
Apply clippy suggestion.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-03-16 10:49:37 -05:00
Edu4rdSHL
83c75b753b
Update builder.sh
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-03-11 15:32:30 -05:00
Edu4rdSHL
c646e9c5d9
Fix #7 - now we depend on the Findomain's trust-dns crate.
Bump dependencies.

Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-02-20 08:16:49 -05:00
Edu4rdSHL
a41b1807cb
Update builder.sh
Cargo.toml: add [profile.release]

Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-02-13 06:08:27 -05:00
Eduard Tolosa
661c4b1a04
Update installation instructions and remove legacy documentation. 2022-01-04 09:39:36 -05:00
Edu4rdSHL
970fd42e48
Fix clippy warnings.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-10-20 06:16:13 -05:00
Amin Vakil
8543aa2c44
Fix grammar. 2021-09-06 01:24:05 -05:00
Eduard Tolosa
21bc360f88
Update FUNDING.yml 2021-07-26 09:10:08 +00:00
0xflotus
1e57858d4d
Fix typos. 2021-05-21 23:20:36 -05:00
Edu4rdSHL
89e300439a
Bump version. 2021-05-16 16:23:45 -05:00
Edu4rdSHL
4ffa94d613
Bump version. 2021-05-16 07:44:28 -05:00
Edu4rdSHL
ab00d977bd
Use a Resolver with multiple nameserver ips instead of creating multiple Resolver instances.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-16 07:40:29 -05:00
Edu4rdSHL
fb6dbe11d0
Use custom trust-dns fork for performance improvements.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-16 07:37:45 -05:00
Edu4rdSHL
9845fc4481
Bump version. 2021-05-11 15:03:52 -05:00
Edu4rdSHL
ba1f7da885
Add support from reading from stdin.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-11 15:00:50 -05:00
Edu4rdSHL
177e29abe4
Update .gitignore.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-11 03:18:48 -05:00
Edu4rdSHL
b969fd74e9
Bump version. 2021-05-10 21:08:25 -05:00
Edu4rdSHL
1b79bcd334
Update args.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-10 20:39:29 -05:00
Edu4rdSHL
b8a47031d9
Add version detection.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-10 20:36:06 -05:00
Edu4rdSHL
8378a29d5d
Add support for raw and url output formats.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-10 19:25:53 -05:00
Edu4rdSHL
f20f6d6ce0
Add raw output support.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-10 11:58:50 -05:00
24 changed files with 3021 additions and 1416 deletions

5
.github/FUNDING.yml vendored
View file

@ -1,4 +1 @@
# These are supported funding model platforms
patreon: edu4rdshl
open_collective: findomain
custom: ["https://paypal.me/sechacklabs"]
github: [edu4rdshl]

36
.github/workflows/cicd-to-dockerhub.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: ci-to-dockerhub
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/unimap:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

128
.github/workflows/make-release.yml vendored Normal file
View file

@ -0,0 +1,128 @@
name: Create and upload artifacts
on:
workflow_dispatch:
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
include:
- type: ubuntu-x64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
name: unimap-linux
path: target/x86_64-unknown-linux-musl/release/unimap
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: ubuntu-x86
os: ubuntu-latest
target: i686-unknown-linux-musl
name: unimap-linux-i386
path: target/i686-unknown-linux-musl/release/unimap
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
- type: armv7
os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
name: unimap-armv7
path: target/armv7-unknown-linux-gnueabihf/release/unimap
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
name: unimap-aarch64
path: target/aarch64-unknown-linux-gnu/release/unimap
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps:
- uses: actions/checkout@v2
- name: Install System Dependencies
run: |
env
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
env:
PKG_CONFIG_PATH: ${{ matrix.pkg_config_path }}
OPENSSL_DIR: /usr/lib/ssl
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- name: Strip symbols from binary
run: |
strip -s ${{ matrix.path }} || arm-linux-gnueabihf-strip -s ${{ matrix.path }} || aarch64-linux-gnu-strip -s ${{ matrix.path }}
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-apple-darwin
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=x86_64-apple-darwin
- name: Strip symbols from binary
run: |
strip -u -r target/x86_64-apple-darwin/release/unimap
- uses: actions/upload-artifact@v2
with:
name: unimap-osx
path: target/x86_64-apple-darwin/release/unimap
- uses: actions/upload-artifact@v2
with:
name: unimap-osx
path: unimap-osx
build-windows:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
strategy:
matrix:
type: [windows-x64, windows-x86]
include:
- type: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
name: unimap-windows.exe
path: target\x86_64-pc-windows-msvc\release\unimap.exe
- type: windows-x86
os: windows-latest
target: i686-pc-windows-msvc
name: unimap-windows-i686.exe
path: target\i686-pc-windows-msvc\release\unimap.exe
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target
/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"

3072
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "unimap"
version = "0.3.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,30 +12,39 @@ 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.2"
rayon = "1.5.0"
log = { version = "0.4.14", features = ["std"] }
colored = { version = "2.0.0", optional = true }
rand = "0.8.3"
lazy_static = "1.4.0"
serde = { version = "1.0.125", features = ["derive"] }
serde_derive = "1.0.125"
prettytable-rs = "0.8.0"
serde-xml-rs = "0.4.1"
chrono = "0.4.19"
clap = { version = "2.33.3", 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"
[target.arm-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.34", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.aarch64-unknown-linux-gnu.dependencies]
openssl = { version = "0.10.34", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.armv7-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.34", features = ["vendored"] }
openssl = { version = "0.10.73", features = ["vendored"] }
[target.'cfg(windows)'.dependencies]
atty = "0.2.14"
winapi = { version = "0.3.9", features = ["handleapi", "winbase"] }
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true

14
Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM rust:alpine as builder
RUN apk add --no-cache build-base
WORKDIR /usr/src/unimap
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
RUN cargo install --path .
FROM alpine:3.12
RUN apk add --no-cache nmap nmap-scripts wget
COPY --from=builder /usr/local/cargo/bin/unimap /usr/local/bin/unimap
ENTRYPOINT [ "/usr/local/bin/unimap" ]

View file

@ -2,31 +2,61 @@
Scan only once by IP address and reduce scan times with Nmap for large amounts of data. Unimap is an abbreviation of "Unique Nmap *Scan*". The tool can run in Linux, OSX, Windows or Android (Termux) without problems.
# Why?
If you have plans to run an Nmap to a whole organization you need to consideer that surely tens, hundreds or even thousands of subdomains will point to the same IP address and there will come a point where it becomes almost impossible to continue scanning. Also your IP may end up blocked due to multiple scans to the same remote host address among other things.
If you have plans to run a Nmap to a whole organization you need to consider that surely tens, hundreds or even thousands of subdomains will point to the same IP address and there will come a point where it becomes almost impossible to continue scanning. Also your IP may end up blocked due to multiple scans to the same remote host address among other things.
# How?
Unimap uses its own technology to initially resolve the IP addresses of all subdomains, once this process is finished, it creates a vector with the unique IP addresses and launches a parallel scan with Nmap according to the number of threads that the user has configured, at the same time, it analyzes the data from the files created with Nmap to find out which information corresponds to each IP. Finally, Unimap relates the information of each of the IPs associated with the subdomains. So, for example, if you have 50 subdomains that point to the same IP, you will only do one Nmap scan but you will have all the data associated with each of the subdomains at the same time, in a large scan it saves days or weeks.
# Installation
## From source
You need to have Rust and Nmap installed in your computer, then run:
* `cargo install unimap` to install from [crates.io](https://crates.io).
* or
```
1. git clone https://github.com/Edu4rdSHL/unimap.git && cd unimap
# Alternatively you can download a release from https://github.com/Edu4rdSHL/unimap/releases/latest
# extract it and continue to next step.
2. cargo build --release
# Now the binary is in ./target/release/unimap
# The next command only works on Linux and MacOS. Windows user need specify the target path in environment variable
3. cp ./target/release/unimap /usr/local/bin
# Now you can use the `unimap` command from everyewhere.
```
## Docker support
You have two options to install Findomain in a docker container.
**Using Dockerhub (recommended):**
```
1. docker pull edu4rdshl/unimap:latest
3. docker run -it --rm --name unimap unimap -t hackerone.com --fast-scan
# Set alias in ~/.bashrc or ~/.zshrc for global use
4. alias unimap='docker run -it --rm --name unimap unimap'
```
**Building the Docker image yourself:**
```
1. git clone https://github.com/Edu4rdSHL/unimap.git && cd unimap
2. docker build --tag unimap .
3. docker run -it --rm --name unimap unimap -t hackerone.com --fast-scan
# Set alias in ~/.bashrc or ~/.zshrc for global use
4. alias unimap='docker run -it --rm --name unimap unimap'
```
## Using precompiled binaries
Download the [latest version](https://github.com/Edu4rdSHL/unimap/releases/latest) for your OS and use it.
## Using the AUR packages. (Arch Linux)
`unimap` can be installed from available [AUR packages](https://aur.archlinux.org/packages/?O=0&SeB=b&K=unimap&outdated=&SB=n&SO=a&PP=50&do_Search=Go) using an [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers). For example,
`unimap` can be installed from available [AUR packages](https://aur.archlinux.org/packages/?O=0&SeB=b&K=unimap&outdated=&SB=n&SO=a&PP=50&do_Search=Go) using a [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers). For example,
```
yay -S unimap
$ paru -S unimap
```
If you prefer, you can clone the [AUR packages](https://aur.archlinux.org/packages/?O=0&SeB=b&K=unimap&outdated=&SB=n&SO=a&PP=50&do_Search=Go) and then compile them with [makepkg](https://wiki.archlinux.org/index.php/Makepkg). For example,
@ -39,39 +69,17 @@ git clone https://aur.archlinux.org/unimap.git && cd unimap && makepkg -si
**Unimap requires root/administrator privileges to launch [Nmap TCP SYN (Stealth) Scan](https://nmap.org/book/synscan.html), we use it for accuracy and performance reasons.** If you are on Linux or Linux-based, just use a root shell or run the tool with sudo, in Windows you can open a Command Prompt (CMD) as Administrator and run the tool as usual.
* Flags doesn't require/accept values.
|Flag|Description|
|-----------|------------|
|--fast-scan|Use fast scanning for ports (no version detection) and just scan the top 1000 ports.|
|-o, --output| Write to an output file. The name of the output file will be unimap-log-$date.|
|-k, --keep-nmap-logs| Keep Nmap XML files created in the logs/ folder for every scanned IP. This data will be useful for other tasks.|
* Options require values.
|Option|Description|
|-----------|------------|
|--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.|
|-f, --file|Use a list of targets writen in a file as input.|
|--iport|Initial port if you want to scan a custom range.|
|--lport|Last port if you want to scan a custom range.|
|--logs-dir|Path to save the CSV data of the process. Usefull when doing automated scans with the -o option.|
|--min-rate| Controls [Nmap's --min-rate](https://nmap.org/book/man-performance.html). Default to 30000.|
|-t, --target|Host if you want to scan only one and extract the interesting data.|
|--threads|Number of threads. **Have in mind that the same number of Nmap scans will be started at time.** Default to 30.|
|-u, --unique-output|Write output to an specified filename.|
# Examples
1. `unimap -f targets.txt -u log.csv` performs an full scan and write output to log.csv.
2. `unimap -f targets.txt --fast-scan -o` performs an fast scan and save the logfile to logs/ folder.
3. `sudo unimap -f targets.txt --iport 1 --lport 1000 --min-rate 1000` scan ports from 1-1000 doing service and versions detection (if you want a fast scan use the --fast-scan flag) with an min-rate of 1000.
1. `unimap -f targets.txt -u log.csv` performs a full scan and writes output to log.csv.
2. `unimap -f targets.txt --fast-scan -o` performs a fast scan and saves the logfile to the logs/ folder.
3. `sudo unimap -f targets.txt --ports "1-1000" --min-rate 5000` scans ports from 1-1000 doing service and version detection (if you want a fast scan use the --fast-scan flag) with a min-rate of 5000.
# Considerations
* Unimap is preconfigured to run on faster networks (cloud VPS), if you run a scan in a home network that doesn't have too much capacity you will end up disconnected from network.
* Unimap is preconfigured to run on faster networks (cloud VPS), if you run a scan in a home network that doesn't have too much capacity you will end up disconnected due to network throttling.
* The previously doesn't mean you can not use Unimap from your home, just adjust the number of `--threads` and `--min-rate` (being it the most important).
* We do not wrap Nmap in any way or scan ports on our own, we use the right Nmap options to get the most performance, Nmap rocks and it's the faster ports scanner that currently exists.
* We do not wrap Nmap in any way or scan ports on our own, we use the right Nmap options to get the most performance, Nmap rocks and it's the fastest port scanner that currently exists.
* We parse Nmap output data and give you more understandable output while also preventing you to scan the same IP several times, **it's our main goal** .
# Found a bug?

97
builder.sh Executable file
View file

@ -0,0 +1,97 @@
#!/usr/bin/env bash
NAME="unimap"
LINUX_TARGET="x86_64-unknown-linux-musl"
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"
MANPAGE_DIR="./$NAME.1"
BIN_OUTPUT_DIR="./ghbinaries"
rm -rf "$BIN_OUTPUT_DIR"
mkdir -p "$BIN_OUTPUT_DIR"
# Linux build
echo "Building Linux artifact."
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" >"$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
# Linux x86 build
echo "Building Linux x86 artifact."
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" >"$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
# Windows build
echo "Building Windows artifact."
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" >"$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
# ARMV7 build
echo "Building ARMv7 artifact."
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" >"$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
# Aarch64 build
echo "Building Aarch64 artifact."
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"
# 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 successfully and saved in $MANPAGE_DIR"
else
echo "Error creating manpage."
fi
else
echo "Please install the help2man package."
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(|_| "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(|_| "logs".to_string()),
threads: if (matches.is_present("port-scan")
|| matches.is_present("initial-port")
|| matches.is_present("last-port"))
&& !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("initial-port") || matches.is_present("last-port"),
fast_scan: matches.is_present("fast-scan"),
keep_nmap_logs: matches.is_present("keep-nmap-logs"),
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,85 +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
- 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
- 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
- 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: keep-nmap-logs
takes_value: false
multiple: false

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(_) => (),
match std::fs::remove_file(file) {
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

@ -11,11 +11,11 @@ pub mod args;
pub mod errors;
pub mod files;
pub mod logger;
pub mod misc;
pub mod resolver_engine;
mod defaults;
mod logic;
mod misc;
mod networking;
mod nmap;
mod structs;

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,25 +1,30 @@
use {
clap::Parser,
log::{error, Level},
std::{collections::HashSet, iter::FromIterator},
unimap::{args, errors::*, files::return_file_targets, logger, 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()))
} else {
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();
}
if arguments.targets.len() < 50 {
arguments.threads = arguments.targets.len()
arguments.threads = arguments.targets.len();
}
rayon::ThreadPoolBuilder::new()
@ -27,19 +32,19 @@ fn run() -> Result<()> {
.build_global()
.unwrap();
if !arguments.target.is_empty() || !arguments.files.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

@ -1,34 +1,22 @@
use std::collections::HashSet;
use std::{
collections::HashSet,
io::{self, Read},
};
#[must_use]
pub fn sanitize_target_string(target: String) -> String {
target
.replace("www.", "")
.replace("https://", "")
.replace("http://", "")
.replace("/", "")
.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();
stdin
.read_to_string(&mut buffer)
.expect("Error getting input list.");
buffer.lines().map(str::to_owned).collect()
}

View file

@ -1,47 +1,68 @@
use {
log::error,
rand::{seq::SliceRandom, thread_rng as rng},
std::net::{IpAddr, Ipv4Addr},
trust_dns_resolver::{
config::{NameServerConfigGroup, 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.")
},
)
}
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: &ProcessedArgs) -> HashSet<SocketAddr> {
let mut resolver_ips = HashSet::new();
if args.custom_resolvers {
for r in &files::return_file_targets(args, args.resolvers.clone()) {
let server = r.to_owned() + ":53";
let socket_addr = SocketAddr::V4(match server.parse() {
Ok(a) => a,
Err(e) => unreachable!(
"Error parsing the server {}, only IPv4 are allowed. Error: {}",
r, e
),
});
resolver_ips.insert(socket_addr);
}
} else {
String::new()
}
}
pub fn get_resolver(resolvers_ips: &[Ipv4Addr], opts: &ResolverOpts) -> Resolver {
match Resolver::new(
ResolverConfig::from_parts(
None,
vec![],
NameServerConfigGroup::from_ips_clear(
&[IpAddr::V4(
resolvers_ips
.choose(&mut rng())
.expect("failed to read ipv4 string")
.to_owned(),
)],
53,
false,
for r in &args.resolvers {
let server = r.to_owned() + ":53";
let socket_addr = SocketAddr::V4(match server.parse() {
Ok(a) => a,
Err(e) => unreachable!(
"Error parsing the server {}, only IPv4 are allowed. Error: {}",
r, e
),
),
*opts,
) {
Ok(resolver) => resolver,
Err(e) => {
error!("Failed to create the resolver. Error: {}\n", e);
std::process::exit(1)
});
resolver_ips.insert(socket_addr);
}
}
resolver_ips
}

View file

@ -1,25 +1,15 @@
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(|f| f.to_string())
.collect::<Vec<String>>()
.join(",");
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub nmaprun: Nmaprun,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Nmaprun {
pub scanner: String,
@ -31,7 +21,7 @@ pub struct Nmaprun {
pub host: Option<Host>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Host {
pub starttime: String,
@ -42,7 +32,7 @@ pub struct Host {
pub ports: Option<Ports>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Status {
pub state: String,
@ -51,20 +41,20 @@ pub struct Status {
pub reason_ttl: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Address {
pub addr: Option<String>,
pub addrtype: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hostnames {
pub hostname: Option<Hostname>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hostname {
pub name: String,
@ -72,7 +62,7 @@ pub struct Hostname {
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ports {
pub port: Vec<Port>,
@ -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,15 +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"]);
}
if !ports.is_empty() {
nmap_args.append(&mut vec!["-p", &ports])
nmap_args.append(&mut vec!["-p", ports]);
}
nmap_args.push(host);
@ -157,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,49 +16,55 @@ use {
net::Ipv4Addr,
time::Duration,
},
trust_dns_resolver::config::ResolverOpts,
};
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 data = parallel_resolver_engine(&args, args.targets.clone());
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, &resolver);
let mut table = Table::new();
table.set_titles(row![
@ -66,8 +73,55 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
"OPEN PORTS",
"SERVICES"
]);
if args.raw_output && !args.quiet_flag {
println!("HOST,IP,PORT,SERVICE,VERSION,PRODUCT,OS,EXTRAINFO");
} else if args.url_output && !args.quiet_flag {
println!("HOST:IP");
}
for (target, resolv_data) in &data {
if !resolv_data.ip.is_empty() {
if args.raw_output {
for port_data in &resolv_data.ports_data {
println!(
"{},{},{},{},{},{},{},{}",
target,
resolv_data.ip,
port_data.portid,
port_data.service.clone().unwrap_or_default().name,
port_data
.clone()
.service
.unwrap_or_default()
.version
.unwrap_or_else(|| "NULL".to_string()),
port_data
.clone()
.service
.clone()
.unwrap_or_default()
.product
.unwrap_or_else(|| "NULL".to_string()),
port_data
.service
.clone()
.unwrap_or_default()
.ostype
.clone()
.unwrap_or_else(|| "NULL".to_string()),
port_data
.service
.clone()
.unwrap_or_default()
.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);
}
} else {
let mut services_table = Table::new();
for port_data in &resolv_data.ports_data {
services_table
@ -110,18 +164,19 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
]);
}
}
}
if args.with_output
&& !args.targets.is_empty()
&& files::table_to_file(&table, files::return_output_file(&args)).is_err()
&& files::table_to_file(&table, files::return_output_file(args)).is_err()
&& !args.quiet_flag
{
error!(
"An error occurred while writing the output file {}.\n",
args.file_name
)
);
}
if !args.quiet_flag {
if !args.quiet_flag && !args.raw_output && !args.url_output {
table.printstd();
}
@ -132,39 +187,58 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
);
info!("Logfile saved in {}\n\n", args.file_name);
}
if !args.quiet_flag {
println!();
}
Ok(())
}
fn parallel_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<String, ResolvData> {
let opts = ResolverOpts {
timeout: Duration::from_secs(2),
..Default::default()
};
fn parallel_resolver_engine(
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(&networking::get_resolver(&RESOLVERS, &opts), &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
.iter()
.map(|(_, resolv_data)| resolv_data.ip.clone())
.values()
.map(|resolv_data| resolv_data.ip.clone())
.collect();
nmap_ips.retain(|ip| !ip.is_empty());
let nmap_ips_orig = nmap_ips.clone();
nmap_ips.retain(|ip| {
!ip.is_empty()
&& !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
@ -174,8 +248,8 @@ fn parallel_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<St
.unwrap_or_default()
.port
.retain(|f| f.state.state == "open");
if !args.keep_nmap_logs && std::fs::remove_file(&filename).is_err() {
error!("Error removing filename {}.", &filename)
if args.no_keep_nmap_logs && std::fs::remove_file(&filename).is_err() {
error!("Error removing filename {}.", &filename);
}
(ip.clone(), nmap_data)
}
@ -187,6 +261,11 @@ fn parallel_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<St
})
.collect();
// 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);
}
resolv_data
.iter()
.map(|(target, resolv_data)| {
@ -212,4 +291,5 @@ fn parallel_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<St
)
})
.collect()
}
}

View file

@ -1,30 +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 keep_nmap_logs: bool,
pub fast_scan: 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 {
@ -32,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(),
}

73
unimap.1 Normal file
View file

@ -0,0 +1,73 @@
.\" 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.7.0
.SH SYNOPSIS
.B unimap
[\fI\,OPTIONS\/\fR]
.SH DESCRIPTION
Scan only once by IP address and reduce scan times with Nmap for large amounts of data.
.SH OPTIONS
.TP
\fB\-t\fR, \fB\-\-target\fR <TARGET>
Target host
.TP
\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
.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
.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\-\-url\-output\fR
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
Print version
.SH "SEE ALSO"
The full documentation for
.B unimap
is maintained as a Texinfo manual. If the
.B info
and
.B unimap
programs are properly installed at your site, the command
.IP
.B info unimap
.PP
should give you access to the complete manual.