Compare commits

...

33 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
23 changed files with 2815 additions and 1509 deletions

5
.github/FUNDING.yml vendored
View file

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

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 }}

1
.gitignore vendored
View file

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

2
.vscode/launch.json vendored
View file

@ -9,7 +9,7 @@
"request": "launch", "request": "launch",
"name": "Debug", "name": "Debug",
"program": "${workspaceRoot}/target/debug/${workspaceRootFolderName}", "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}", "cwd": "${workspaceRoot}",
"sourceLanguages": [ "sourceLanguages": [
"rust" "rust"

3055
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "unimap" name = "unimap"
version = "0.5.1" version = "0.7.0"
authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"] authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"]
edition = "2018" edition = "2018"
description = "Scan only once by IP address and reduce scan times with Nmap for large amounts of data." 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
trust-dns-resolver = { git = "https://github.com/Edu4rdSHL/trust-dns", branch = "main" } hickory-resolver = "0.25.2"
rayon = "1.5.0" rayon = "1.10.0"
log = { version = "0.4.14", features = ["std"] } log = { version = "0.4.27", features = ["std"] }
colored = { version = "2.0.0", optional = true } colored = { version = "3.0.0", optional = true }
rand = "0.8.3" rand = "0.9.2"
lazy_static = "1.4.0" lazy_static = "1.5.0"
serde = { version = "1.0.125", features = ["derive"] } tokio = { version = "1.47.0", features = ["rt"] }
serde_derive = "1.0.125" futures = "0.3"
prettytable-rs = "0.8.0" serde = { version = "1.0.219", features = ["derive"] }
serde-xml-rs = "0.4.1" serde_derive = "1.0.219"
chrono = "0.4.19" prettytable-rs = "0.10.0"
clap = { version = "2.33.3", features = ["yaml"] } serde-xml-rs = "0.8.1"
config = { version = "0.11.0", features = ["yaml", "json", "toml", "hjson", "ini"] } 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" failure = "0.1.8"
[target.arm-unknown-linux-gnueabihf.dependencies] [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] [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] [target.armv7-unknown-linux-gnueabihf.dependencies]
openssl = { version = "0.10.34", features = ["vendored"] } openssl = { version = "0.10.73", features = ["vendored"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
atty = "0.2.14" atty = "0.2.14"
winapi = { version = "0.3.9", features = ["handleapi", "winbase"] } 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. 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? # 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? # 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. 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 # Installation
## From source
You need to have Rust and Nmap installed in your computer, then run: 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 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 # Alternatively you can download a release from https://github.com/Edu4rdSHL/unimap/releases/latest
# extract it and continue to next step. # extract it and continue to next step.
2. cargo build --release 2. cargo build --release
# Now the binary is in ./target/release/unimap # 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) ## 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, 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. **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 # Examples
1. `unimap -f targets.txt -u log.csv` performs an full scan and write output to log.csv. 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 an fast scan and save the logfile to logs/ folder. 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 --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. 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 # 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). * 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** . * 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? # Found a bug?

View file

@ -1,28 +1,50 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Rusolver releaser
NAME="unimap" NAME="unimap"
LINUX_TARGET="x86_64-unknown-linux-musl" LINUX_TARGET="x86_64-unknown-linux-musl"
LINUX_X86_TARGET="i686-unknown-linux-musl"
WIN_TARGET="x86_64-pc-windows-gnu" WIN_TARGET="x86_64-pc-windows-gnu"
ARMV7_TARGET="armv7-unknown-linux-gnueabihf" ARMV7_TARGET="armv7-unknown-linux-gnueabihf"
AARCH_TARGET="aarch64-unknown-linux-gnu" AARCH_TARGET="aarch64-unknown-linux-gnu"
OSX_TARGET="x86_64-apple-darwin" # OSX_TARGET="x86_64-apple-darwin"
MANPAGE_DIR="./$NAME.1" MANPAGE_DIR="./$NAME.1"
BIN_OUTPUT_DIR="./ghbinaries"
rm -rf "$BIN_OUTPUT_DIR"
mkdir -p "$BIN_OUTPUT_DIR"
# Linux build # Linux build
echo "Building Linux artifact." 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" echo "Linux artifact build: SUCCESS"
cp "target/$LINUX_TARGET/release/$NAME" "target/$LINUX_TARGET/release/$NAME-linux" 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 else
echo "Linux artifact build: FAILED" echo "Linux artifact build: FAILED"
fi 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 # Windows build
echo "Building Windows artifact." echo "Building Windows artifact."
if cross build -q --release --target="$WIN_TARGET"; then if cross build -q --release --target="$WIN_TARGET"; then
echo "Windows artifact build: SUCCESS" echo "Windows artifact build: SUCCESS"
cp "target/$WIN_TARGET/release/$NAME.exe" "target/$WIN_TARGET/release/$NAME-windows.exe" 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 else
echo "Windows artifact build: FAILED" echo "Windows artifact build: FAILED"
fi fi
@ -32,6 +54,9 @@ echo "Building ARMv7 artifact."
if cross build -q --release --target="$ARMV7_TARGET"; then if cross build -q --release --target="$ARMV7_TARGET"; then
echo "ARMv7 artifact build: SUCCESS" echo "ARMv7 artifact build: SUCCESS"
cp "target/$ARMV7_TARGET/release/$NAME" "target/$ARMV7_TARGET/release/$NAME-armv7" 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 else
echo "ARMv7 artifact build: FAILED" echo "ARMv7 artifact build: FAILED"
fi fi
@ -41,42 +66,32 @@ echo "Building Aarch64 artifact."
if cross build -q --release --target="$AARCH_TARGET"; then if cross build -q --release --target="$AARCH_TARGET"; then
echo "Aarch64 artifact build: SUCCESS" echo "Aarch64 artifact build: SUCCESS"
cp "target/$AARCH_TARGET/release/$NAME" "target/$AARCH_TARGET/release/$NAME-aarch64" 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 else
echo "Aarch64 artifact build: FAILED" echo "Aarch64 artifact build: FAILED"
fi fi
# Mac OS build # # Mac OS build
echo "Building OSX artifact." # echo "Building OSX artifact."
if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then # if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then
echo "OSX artifact build: SUCCESS" # echo "OSX artifact build: SUCCESS"
cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx" # cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx"
else # strip "target/$OSX_TARGET/release/$NAME-osx"
echo "OSX artifact build: FAILED" # sha512sum "target/$OSX_TARGET/release/$NAME-osx" >"target/$OSX_TARGET/release/$NAME-osx.sha512"
fi # zip -q "$BIN_OUTPUT_DIR/$NAME-osx.zip" "target/$OSX_TARGET/release/$NAME-osx"
# else
# echo "OSX artifact build: FAILED"
# fi
echo "Creating manpage..." echo "Creating manpage..."
if command -v help2man >/dev/null; then if command -v help2man >/dev/null; then
if help2man -o "$MANPAGE_DIR" "target/$LINUX_TARGET/release/$NAME"; 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 else
echo "Error creating manpage." echo "Error creating manpage."
fi fi
else else
echo "Please install the help2man package." echo "Please install the help2man package."
fi fi
if command -v git >/dev/null; then
git add .
git commit -m "Bump version."
git push
fi
#echo "Uploading crate to crates.io..."
#if cargo publish --no-verify > /dev/null; then
# echo "Crate uploaded."
#else
# echo "An error has occurred while uploading the crate to crates.io."
# exit
#fi
echo "All builds have passed!"

View file

@ -1,72 +1,167 @@
use { use {
crate::{ crate::{defaults, logic::validate_target, misc::sanitize_target_string},
defaults,
logic::validate_target,
misc::{return_matches_vec, sanitize_target_string},
structs::Args,
},
chrono::Utc, chrono::Utc,
clap::{load_yaml, value_t, App}, clap::Parser,
std::{collections::HashSet, time::Instant}, std::{collections::HashSet, time::Instant},
}; };
#[allow(clippy::cognitive_complexity)] /// Scan only once by IP address and reduce scan times with Nmap for large amounts of data.
pub fn get_args() -> Args { #[derive(Parser, Debug, Clone)]
let yaml = load_yaml!("cli.yml"); #[command(author = "Eduard Tolosa <edu4rdshl@protonmail.com>", version, about, long_about = None)]
let matches = App::from_yaml(yaml) #[command(arg_required_else_help = true)]
.version(clap::crate_version!()) pub struct Args {
.get_matches(); /// Target host
#[arg(short, long, conflicts_with_all = ["files", "stdin"])]
pub target: Option<String>,
Args { /// Use a list of targets written in a file as input
target: { #[arg(short, long, conflicts_with_all = ["target", "stdin"])]
let target = sanitize_target_string( pub files: Vec<String>,
value_t!(matches, "target", String).unwrap_or_else(|_| String::new()),
); /// Write to an output file. The name of the output file will be unimap-log-date
if validate_target(&target) { #[arg(short, long, conflicts_with = "unique_output")]
target 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 { } else {
String::new() String::new()
} }
}, });
file_name: if matches.is_present("output") {
value_t!(matches, "logs-dir", String).unwrap_or_else(|_| "unimap_logs".to_string()) let file_name = if self.output {
+ "/" format!(
+ "unimap" "{}/unimap{}",
+ &Utc::now().format("-log-%Y-%m-%d_%H-%M-%S").to_string() self.logs_dir,
+ ".csv" Utc::now().format("-log-%Y-%m-%d_%H-%M-%S")
} else if matches.is_present("unique-output") { ) + ".csv"
matches.value_of("unique-output").unwrap().to_string() } else if let Some(unique_output) = &self.unique_output {
unique_output.clone()
} else { } else {
String::new() String::new()
}, };
logs_dir: value_t!(matches, "logs-dir", String)
.unwrap_or_else(|_| "unimap_logs".to_string()), let threads = if self.ports.is_some() && self.threads.is_none() {
threads: if matches.is_present("ports") && !matches.is_present("threads") {
30 30
} else { } else {
value_t!(matches, "threads", usize).unwrap_or_else(|_| 50) self.threads.unwrap_or(50)
}, };
version: clap::crate_version!().to_string(),
ports: value_t!(matches, "ports", String).unwrap_or_else(|_| String::new()), let custom_resolvers_flag = !self.custom_resolvers.is_empty();
with_output: matches.is_present("output") || matches.is_present("unique-output"), let custom_ports_range = self.ports.is_some();
unique_output_flag: matches.is_present("unique-output"), let ports_value = self.ports.unwrap_or_default();
from_file_flag: matches.is_present("files"),
quiet_flag: matches.is_present("quiet"), let resolvers = if custom_resolvers_flag {
custom_resolvers: matches.is_present("custom-resolvers"), self.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")
} else { } else {
defaults::ipv4_resolvers() 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(), targets: HashSet::new(),
time_wasted: Instant::now(), 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 // Verisign
"64.6.64.6", "64.6.64.6",
"64.6.65.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() .into_iter()
.map(str::to_owned) .map(str::to_owned)

View file

@ -1,5 +1,8 @@
use { use {
crate::{errors::*, structs::Args}, crate::{
args::ProcessedArgs,
errors::{Result, ResultExt},
},
log::error, log::error,
prettytable::Table, prettytable::Table,
std::{ 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(); let mut targets: Vec<String> = Vec::new();
files.sort(); files.sort();
files.dedup(); files.dedup();
for f in files { for f in files {
match File::open(&f) { match File::open(&f) {
Ok(file) => { 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); targets.push(target);
} }
} }
Err(e) => { Err(e) => {
if args.files.len() == 1 { 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) std::process::exit(1)
} else if !args.quiet_flag { } else if !args.quiet_flag {
error!( error!("Can not open file {f}, working with next file. Error: {e}\n");
"Can not open file {}, working with next file. Error: {}\n",
f, e
);
} }
} }
} }
@ -44,7 +48,8 @@ pub fn table_to_file(table: &Table, file_name: Option<std::fs::File>) -> Result<
Ok(()) 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 { if args.file_name.is_empty() || !args.with_output {
None None
} else { } 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 { pub fn check_full_path(full_path: &str) -> bool {
(Path::new(full_path).exists() && Path::new(full_path).is_dir()) (Path::new(full_path).exists() && Path::new(full_path).is_dir())
|| fs::create_dir_all(full_path).is_ok() || 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 { for file in paths {
if Path::new(&file).exists() { if Path::new(&file).exists() {
match std::fs::remove_file(&file) { match std::fs::remove_file(file) {
Ok(_) => (), Ok(()) => (),
Err(e) => error!("Error deleting the file {}. Description: {}", &file, e), 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())?; file.write_all(data.as_bytes())?;
Ok(()) Ok(())
} }

View file

@ -1,6 +1,5 @@
#[cfg(windows)] #[cfg(windows)]
extern crate atty; extern crate atty;
#[cfg(feature = "chrono")]
extern crate chrono; extern crate chrono;
#[cfg(feature = "colored")] #[cfg(feature = "colored")]
extern crate colored; extern crate colored;
@ -8,7 +7,6 @@ extern crate log;
#[cfg(windows)] #[cfg(windows)]
extern crate winapi; extern crate winapi;
#[cfg(feature = "chrono")]
use chrono::Local; use chrono::Local;
#[cfg(feature = "colored")] #[cfg(feature = "colored")]
use colored::*; use colored::*;
@ -23,10 +21,10 @@ impl Log for SimpleLogger {
} }
fn log(&self, record: &Record) { fn log(&self, record: &Record) {
let target = if !record.target().is_empty() { let target = if record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default() record.module_path().unwrap_or_default()
} else {
record.target()
}; };
if target.contains("unimap") && self.enabled(record.metadata()) { if target.contains("unimap") && self.enabled(record.metadata()) {
let level_string = { let level_string = {
@ -45,7 +43,6 @@ impl Log for SimpleLogger {
record.level().to_string() record.level().to_string()
} }
}; };
#[cfg(feature = "chrono")]
{ {
print!( print!(
"\n{} [{}] {}", "\n{} [{}] {}",
@ -54,10 +51,6 @@ impl Log for SimpleLogger {
record.args() 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.starts_with('.')
&& target.contains('.') && target.contains('.')
&& !target.contains(&SPECIAL_CHARS[..]) && !target.contains(&SPECIAL_CHARS[..])
&& target.chars().all(|c| c.is_ascii()) && target.is_ascii()
} }
pub fn null_ip_checker(ip: &str) -> String { pub fn null_ip_checker(ip: &str) -> String {

View file

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

View file

@ -3,37 +3,13 @@ use std::{
io::{self, Read}, io::{self, Read},
}; };
#[must_use]
pub fn sanitize_target_string(target: String) -> String { pub fn sanitize_target_string(target: String) -> String {
target target
.replace("www.", "") .replace("www.", "")
.replace("https://", "") .replace("https://", "")
.replace("http://", "") .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> { pub fn read_stdin() -> HashSet<String> {

View file

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

View file

@ -1,25 +1,15 @@
use { use {
crate::resolver_engine,
log::error, log::error,
std::{path::Path, process::Command}, std::{net::Ipv4Addr, path::Path, process::Command},
}; };
lazy_static! { #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
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)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Root { pub struct Root {
pub nmaprun: Nmaprun, pub nmaprun: Nmaprun,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Nmaprun { pub struct Nmaprun {
pub scanner: String, pub scanner: String,
@ -31,7 +21,7 @@ pub struct Nmaprun {
pub host: Option<Host>, pub host: Option<Host>,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Host { pub struct Host {
pub starttime: String, pub starttime: String,
@ -42,7 +32,7 @@ pub struct Host {
pub ports: Option<Ports>, pub ports: Option<Ports>,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Status { pub struct Status {
pub state: String, pub state: String,
@ -51,20 +41,20 @@ pub struct Status {
pub reason_ttl: String, pub reason_ttl: String,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Address { pub struct Address {
pub addr: Option<String>, pub addr: Option<String>,
pub addrtype: 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")] #[serde(rename_all = "camelCase")]
pub struct Hostnames { pub struct Hostnames {
pub hostname: Option<Hostname>, pub hostname: Option<Hostname>,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Hostname { pub struct Hostname {
pub name: String, pub name: String,
@ -72,7 +62,7 @@ pub struct Hostname {
pub type_field: String, pub type_field: String,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Ports { pub struct Ports {
pub port: Vec<Port>, pub port: Vec<Port>,
@ -114,12 +104,19 @@ pub fn get_nmap_data(
min_rate: &str, min_rate: &str,
ports: &str, ports: &str,
fast_scan: bool, fast_scan: bool,
resolvers: &[Ipv4Addr],
) -> Result<Nmaprun, serde_xml_rs::Error> { ) -> Result<Nmaprun, serde_xml_rs::Error> {
let min_rate = min_rate.to_string(); 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![ let mut nmap_args = vec![
"nmap", "nmap",
"--dns-servers", "--dns-servers",
&NMAP_DNS_RESOLVERS, &nmap_dns_resolvers,
"-Pn", "-Pn",
"-sS", "-sS",
"--open", "--open",
@ -132,17 +129,17 @@ pub fn get_nmap_data(
]; ];
if !min_rate.is_empty() { 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 { if fast_scan {
nmap_args.append(&mut vec!["--host-timeout", "20m"]) nmap_args.append(&mut vec!["--host-timeout", "20m"]);
} else { } else {
nmap_args.append(&mut vec!["-sV"]) nmap_args.append(&mut vec!["-sV"]);
} }
if !ports.is_empty() { if !ports.is_empty() {
nmap_args.append(&mut vec!["-p", &ports]) nmap_args.append(&mut vec!["-p", ports]);
} }
nmap_args.push(host); nmap_args.push(host);
@ -159,8 +156,7 @@ pub fn get_nmap_data(
} }
Err(e) => { Err(e) => {
error!( error!(
"Error waiting command to finish for {}, continuing with remaining hosts. Description: {}", "Error waiting command to finish for {host}, continuing with remaining hosts. Description: {e}"
host, e
); );
Ok(Nmaprun::default()) Ok(Nmaprun::default())
} }

View file

@ -1,11 +1,12 @@
use { use {
crate::{ crate::{
args, args::ProcessedArgs,
errors::*, errors::Result,
files, logic, networking, files, logic, networking,
nmap::{self, Nmaprun}, nmap::{self, Nmaprun},
structs::{Args, ResolvData}, structs::ResolvData,
}, },
hickory_resolver::config::{LookupIpStrategy, ResolverOpts},
log::{error, info}, log::{error, info},
prettytable, prettytable,
prettytable::Table, prettytable::Table,
@ -15,61 +16,55 @@ use {
net::Ipv4Addr, net::Ipv4Addr,
time::Duration, time::Duration,
}, },
trust_dns_resolver::{
config::{LookupIpStrategy, ResolverOpts},
Resolver,
},
}; };
lazy_static! { fn create_resolvers(args: &ProcessedArgs) -> Vec<Ipv4Addr> {
pub static ref RESOLVERS: Vec<Ipv4Addr> = {
let args = args::get_args();
let mut resolver_ips = Vec::new(); let mut resolver_ips = Vec::new();
if args.custom_resolvers { 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>() { match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip), Ok(ip) => resolver_ips.push(ip),
Err(e) => { 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) std::process::exit(1)
} }
} }
} }
} else { } else {
for r in args.resolvers { for r in &args.resolvers {
match r.parse::<Ipv4Addr>() { match r.parse::<Ipv4Addr>() {
Ok(ip) => resolver_ips.push(ip), Ok(ip) => resolver_ips.push(ip),
Err(e) => { 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) std::process::exit(1)
} }
} }
} }
} }
resolver_ips resolver_ips
};
} }
pub fn parallel_resolver_all(args: &mut Args) -> Result<()> { pub fn parallel_resolver_all(args: &mut ProcessedArgs) -> Result<()> {
files::check_full_path(&args.logs_dir); 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 { if !args.quiet_flag {
info!( info!(
"Performing parallel resolution for {} targets with {} threads, it will take a while...\n", "Performing parallel resolution for {} targets with {} threads, it will take a while...\n",
args.targets.len(), args.threads args.targets.len(), args.threads
) );
} }
let opts = ResolverOpts { let mut opts = ResolverOpts::default();
timeout: Duration::from_secs(1), opts.timeout = Duration::from_secs(1);
ip_strategy: LookupIpStrategy::Ipv4Only, opts.ip_strategy = LookupIpStrategy::Ipv4Only;
num_concurrent_reqs: 1, opts.num_concurrent_reqs = 1;
..Default::default()
};
let resolver = networking::get_resolver(networking::return_socket_address(args), opts); 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(); let mut table = Table::new();
table.set_titles(row![ table.set_titles(row![
@ -79,9 +74,9 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
"SERVICES" "SERVICES"
]); ]);
if args.raw_output && !args.quiet_flag { 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 { } else if args.url_output && !args.quiet_flag {
println!("HOST:IP") println!("HOST:IP");
} }
for (target, resolv_data) in &data { for (target, resolv_data) in &data {
if !resolv_data.ip.is_empty() { if !resolv_data.ip.is_empty() {
@ -120,11 +115,11 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
.extrainfo .extrainfo
.clone() .clone()
.unwrap_or_else(|| "NULL".to_string()) .unwrap_or_else(|| "NULL".to_string())
) );
} }
} else if args.url_output { } else if args.url_output {
for port_data in &resolv_data.ports_data { for port_data in &resolv_data.ports_data {
println!("{}:{}", target, port_data.portid) println!("{}:{}", target, port_data.portid);
} }
} else { } else {
let mut services_table = Table::new(); let mut services_table = Table::new();
@ -173,13 +168,13 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
if args.with_output if args.with_output
&& !args.targets.is_empty() && !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 && !args.quiet_flag
{ {
error!( error!(
"An error occurred while writing the output file {}.\n", "An error occurred while writing the output file {}.\n",
args.file_name args.file_name
) );
} }
if !args.quiet_flag && !args.raw_output && !args.url_output { if !args.quiet_flag && !args.raw_output && !args.url_output {
table.printstd(); table.printstd();
@ -199,32 +194,51 @@ pub fn parallel_resolver_all(args: &mut Args) -> Result<()> {
} }
fn parallel_resolver_engine( fn parallel_resolver_engine(
args: &Args, args: &ProcessedArgs,
targets: HashSet<String>, targets: &HashSet<String>,
resolver: Resolver, resolver: &hickory_resolver::TokioResolver,
) -> HashMap<String, ResolvData> { ) -> HashMap<String, ResolvData> {
let resolv_data: HashMap<String, ResolvData> = targets let resolv_data: HashMap<String, ResolvData> = targets
.par_iter() .par_iter()
.map(|target| { .map(|target| {
let fqdn_target = format!("{}.", target); let fqdn_target = format!("{target}.");
let mut resolv_data = ResolvData::default(); 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) (target.to_owned(), resolv_data)
}) })
.collect(); .collect();
let resolvers = create_resolvers(args);
let mut nmap_ips: HashSet<String> = resolv_data let mut nmap_ips: HashSet<String> = resolv_data
.iter() .values()
.map(|(_, resolv_data)| resolv_data.ip.clone()) .map(|resolv_data| resolv_data.ip.clone())
.collect(); .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 let nmap_data: HashMap<String, Nmaprun> = nmap_ips
.par_iter() .par_iter()
.map(|ip| { .map(|ip| {
let filename = format!("{}/{}.xml", &args.logs_dir, &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) => { Ok(nmap_data) => {
nmap_data nmap_data
.host .host
@ -235,7 +249,7 @@ fn parallel_resolver_engine(
.port .port
.retain(|f| f.state.state == "open"); .retain(|f| f.state.state == "open");
if args.no_keep_nmap_logs && std::fs::remove_file(&filename).is_err() { 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) (ip.clone(), nmap_data)
} }
@ -247,6 +261,11 @@ fn parallel_resolver_engine(
}) })
.collect(); .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 resolv_data
.iter() .iter()
.map(|(target, resolv_data)| { .map(|(target, resolv_data)| {
@ -273,3 +292,4 @@ fn parallel_resolver_engine(
}) })
.collect() .collect()
} }
}

View file

@ -1,33 +1,4 @@
use { use crate::nmap::Port;
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,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ResolvData { pub struct ResolvData {
@ -35,8 +6,8 @@ pub struct ResolvData {
pub ports_data: Vec<Port>, pub ports_data: Vec<Port>,
} }
impl ResolvData { impl ResolvData {
pub fn default() -> ResolvData { pub const fn default() -> Self {
ResolvData { Self {
ip: String::new(), ip: String::new(),
ports_data: Vec::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. .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH UNIMAP "1" "May 2021" "Unimap 0.5.1" "User Commands" .TH UNIMAP "1" "August 2025" "unimap 0.7.0" "User Commands"
.SH NAME .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 .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. Scan only once by IP address and reduce scan times with Nmap for large amounts of data.
.SS "USAGE:" .SH OPTIONS
.IP
unimap [FLAGS] [OPTIONS]
.SS "FLAGS:"
.TP .TP
\fB\-\-fast\-scan\fR \fB\-t\fR, \fB\-\-target\fR <TARGET>
Use fast scanning for ports (no version detection). Target host
.TP .TP
\fB\-h\fR, \fB\-\-help\fR \fB\-f\fR, \fB\-\-files\fR <FILES>
Prints help information Use a list of targets written in a file as input
.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 .TP
\fB\-o\fR, \fB\-\-output\fR \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 .TP
\fB\-q\fR, \fB\-\-quiet\fR \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 .TP
\fB\-r\fR, \fB\-\-raw\-output\fR \fB\-r\fR, \fB\-\-raw\-output\fR
Use raw output instead of a table. Use raw output instead of a table
.TP
\fB\-\-stdin\fR
Read from stdin instead of files or aguments.
.TP .TP
\fB\-\-url\-output\fR \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 .TP
\fB\-V\fR, \fB\-\-version\fR \fB\-V\fR, \fB\-\-version\fR
Prints version information Print version
.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.
.SH "SEE ALSO" .SH "SEE ALSO"
The full documentation for The full documentation for
.B Unimap .B unimap
is maintained as a Texinfo manual. If the is maintained as a Texinfo manual. If the
.B info .B info
and and
.B Unimap .B unimap
programs are properly installed at your site, the command programs are properly installed at your site, the command
.IP .IP
.B info Unimap .B info unimap
.PP .PP
should give you access to the complete manual. should give you access to the complete manual.