Compare commits

...

63 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
Edu4rdSHL
e2525b3642
Bump version.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-09 02:21:12 -05:00
Edu4rdSHL
3d24415c9a
Allow ports ranges, lists, or both using the --ports option.
Update the code to match the new dependencies versions.
Simplify some functions.
Improve IP discovery speed calling ipv4_lookup() to search only for A records.

Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2021-05-09 02:13:21 -05:00
Edu4rdSHL
bb8badecf8
Remove clippy warn. 2021-05-09 02:07:09 -05:00
Edu4rdSHL
4b35130fc3
Update DNS to only well-known ones. 2021-05-09 02:06:54 -05:00
Edu4rdSHL
8ad5761f96
Bump dependencies. 2021-05-09 02:06:30 -05:00
Eduard Tolosa
1dd99a054a
README.md: Add AUR instructions. 2020-10-15 10:21:48 -05:00
orhun
5e237d9b7d
Add AUR instructions to README.md 2020-10-15 18:14:19 +03:00
Eduard Tolosa
48cb688376 Fix typo. 2020-09-03 20:44:09 -05:00
Eduard Tolosa
daeafd25c6 Update README. 2020-09-03 20:43:51 -05:00
Eduard Tolosa
39b2e19c33 Update readme. 2020-09-03 20:41:18 -05:00
Eduard Tolosa
448c293dc6 Update gitignore. 2020-09-03 20:32:21 -05:00
Eduard Tolosa
9861d0c925 Fixes. 2020-09-03 20:32:21 -05:00
Eduard Tolosa
945e09bb23 Allow users to keep Nmap XML logs. 2020-09-03 20:32:21 -05:00
Eduard Tolosa
93cbc6a58b Add compatibility with Windows an other OS.
This change adds compatibility with other OS that doesn't have a sudo command.
We enabled ports range that was disabled as well.
2020-09-03 20:32:21 -05:00
Eduard Tolosa
4438718410
Fix typo. 2020-09-03 18:01:39 -05:00
Eduard Tolosa
eae788cda4
Update README.md 2020-09-03 15:23:18 -05:00
Eduard Tolosa
d1a720c554
Update README.md 2020-09-03 15:21:25 -05:00
Eduard Tolosa
a4152f6998
Create FUNDING.yml 2020-09-03 00:29:58 -05:00
Eduard Tolosa
1263155805
Update README.md 2020-09-02 23:58:53 -05:00
24 changed files with 2696 additions and 3502 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
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 }}

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
/target /target
/logs
*.txt *.txt
/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"

2725
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.1.0" 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,31 +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 = "0.19.5" hickory-resolver = "0.25.2"
rayon = "1.4.0" rayon = "1.10.0"
log = { version = "0.4.11", features = ["std"] } log = { version = "0.4.27", features = ["std"] }
colored = { version = "1.9.3", optional = true } colored = { version = "3.0.0", optional = true }
rand = "0.7.3" rand = "0.9.2"
lazy_static = "1.4.0" lazy_static = "1.5.0"
serde = { version = "1.0.114", features = ["derive"] } tokio = { version = "1.47.0", features = ["rt"] }
serde_derive = "1.0.114" futures = "0.3"
prettytable-rs = "0.8.0" serde = { version = "1.0.219", features = ["derive"] }
serde-xml-rs = "0.4.0" serde_derive = "1.0.219"
webpage = "1.1.0" prettytable-rs = "0.10.0"
chrono = "0.4.15" serde-xml-rs = "0.8.1"
clap = { version = "2.33.1", features = ["yaml"] } chrono = "0.4.41"
config = { version = "0.10.1", features = ["yaml", "json", "toml", "hjson", "ini"] } 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.30", 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.30", 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.30", 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

@ -1,55 +1,86 @@
# Unimap # Unimap
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*". 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
You need to have Rust installed, then run: ## 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
``` ```
$ git clone https://github.com/Edu4rdSHL/unimap.git && cd unimap 1. git clone https://github.com/Edu4rdSHL/unimap.git && cd unimap
$ cargo build --release # Alternatively you can download a release from https://github.com/Edu4rdSHL/unimap/releases/latest
$ sudo ./target/release/unimap # 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 a [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers). For example,
```
$ 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,
```
git clone https://aur.archlinux.org/unimap.git && cd unimap && makepkg -si
``` ```
to install from Github.
# Usage # Usage
**Unimap requires root permissions to launch [Nmap TCP SYN (Stealth) Scan](https://nmap.org/book/synscan.html), we use it for accuracy and performance reasons.** See the [nmap.rs](https://github.com/Edu4rdSHL/unimap/blob/43b878e0d1ae74dc44d0ec51ce16a23fc6896a61/src/nmap.rs#L166) module. **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.|
* 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. `$ sudo 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. `$ sudo 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
* 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 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? # Found a bug?
Open an [issue](https://github.com/Edu4rdSHL/unimap). Open an [issue](https://github.com/Edu4rdSHL/unimap).

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 { 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(|_| "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(|_| "logs".to_string()),
threads: if (matches.is_present("port-scan") let threads = if self.ports.is_some() && self.threads.is_none() {
|| matches.is_present("initial-port")
|| matches.is_present("last-port"))
&& !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(),
initial_port: value_t!(matches, "initial-port", usize).unwrap_or_else(|_| 1), let custom_resolvers_flag = !self.custom_resolvers.is_empty();
last_port: value_t!(matches, "last-port", usize).unwrap_or_else(|_| 65535), let custom_ports_range = self.ports.is_some();
with_output: matches.is_present("output") || matches.is_present("unique-output"), let ports_value = self.ports.unwrap_or_default();
unique_output_flag: matches.is_present("unique-output"),
from_file_flag: matches.is_present("files"), let resolvers = if custom_resolvers_flag {
quiet_flag: matches.is_present("quiet"), self.custom_resolvers
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"),
files: return_matches_vec(&matches, "files"),
min_rate: value_t!(matches, "min-rate", usize).unwrap_or_else(|_| 30000),
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,84 +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
- initial-port:
help: Initial port to scan. Default 0.
long: iport
takes_value: true
multiple: false
- last-port:
help: Last port to scan. Default 1000.
long: lport
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. Default to logs/ when using -o.
long: logs-dir
takes_value: true
multiple: false

File diff suppressed because it is too large Load diff

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

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

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

@ -1,5 +1,3 @@
use lazy_static;
lazy_static! { lazy_static! {
static ref SPECIAL_CHARS: Vec<char> = vec![ static ref SPECIAL_CHARS: Vec<char> = vec![
'[', ']', '{', '}', '(', ')', '*', '|', ':', '<', '>', '/', '\\', '%', '&', '¿', '?', '¡', '[', ']', '{', '}', '(', ')', '*', '|', ':', '<', '>', '/', '\\', '%', '&', '¿', '?', '¡',
@ -11,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 {
@ -22,6 +20,7 @@ pub fn null_ip_checker(ip: &str) -> String {
} }
} }
#[allow(clippy::ptr_arg)]
pub fn return_ports_string(ports: &Vec<String>) -> String { pub fn return_ports_string(ports: &Vec<String>) -> String {
if ports.is_empty() { if ports.is_empty() {
String::from("NULL") String::from("NULL")

View file

@ -1,25 +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, 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 { } else if !arguments.target.is_empty() {
arguments.targets.insert(arguments.target.clone()); arguments.targets.insert(arguments.target.clone());
} else {
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()
@ -27,19 +32,19 @@ fn run() -> Result<()> {
.build_global() .build_global()
.unwrap(); .unwrap();
if !arguments.target.is_empty() || !arguments.files.is_empty() { if arguments.targets.is_empty() {
resolver_engine::async_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

@ -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 { 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> { pub fn read_stdin() -> HashSet<String> {
if matches.is_present(value) { let mut buffer = String::new();
matches let mut stdin = io::stdin();
.values_of(value) stdin
.unwrap() .read_to_string(&mut buffer)
.map(str::to_owned) .expect("Error getting input list.");
.collect() buffer.lines().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()
}
} }

View file

@ -1,68 +1,68 @@
use { use {
log::error, crate::{args::ProcessedArgs, files},
rand::Rng, hickory_resolver::{
std::net::{IpAddr, Ipv4Addr}, config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
trust_dns_resolver::{ name_server::TokioConnectionProvider,
config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, proto::xfer::Protocol,
proto::rr::RecordType, TokioResolver,
Resolver,
}, },
std::{collections::HashSet, net::SocketAddr},
}; };
pub fn get_records(resolver: &Resolver, domain: &str, record_type: RecordType) -> String { pub fn get_records(resolver: &TokioResolver, domain: &str) -> String {
if let Ok(rdata) = resolver.lookup(&domain, record_type) { futures::executor::block_on(resolver.ipv4_lookup(domain)).map_or_else(
let mut record_data: Vec<String> = Vec::new(); |_| String::new(),
if record_type == RecordType::AAAA { |ips| {
record_data = rdata ips.iter()
.iter() .map(std::string::ToString::to_string)
.filter_map(|rdata| rdata.as_aaaa())
.map(|ipv6| ipv6.to_string())
.collect();
} else if record_type == RecordType::A {
record_data = rdata
.iter()
.filter_map(|rdata| rdata.as_a())
.map(|ipv4| ipv4.to_string())
.collect();
} else if record_type == RecordType::CNAME {
record_data = rdata
.iter()
.filter_map(|rdata| rdata.as_cname())
.map(|name| {
let name = name.to_string();
name[..name.len() - 1].to_owned()
})
.collect();
}
record_data
.iter()
.next() .next()
.expect("Failed retrieving records data.") .expect("Failed to get IPV4.")
.to_owned() },
)
}
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 { } else {
String::new() for r in &args.resolvers {
} let server = r.to_owned() + ":53";
} let socket_addr = SocketAddr::V4(match server.parse() {
Ok(a) => a,
pub fn get_resolver(resolvers_ips: &[Ipv4Addr], opts: &ResolverOpts) -> Resolver { Err(e) => unreachable!(
match Resolver::new( "Error parsing the server {}, only IPv4 are allowed. Error: {}",
ResolverConfig::from_parts( r, e
None,
vec![],
NameServerConfigGroup::from_ips_clear(
&[IpAddr::V4(
resolvers_ips[rand::thread_rng().gen_range(0, resolvers_ips.len())],
)],
53,
), ),
), });
*opts, resolver_ips.insert(socket_addr);
) {
Ok(resolver) => resolver,
Err(e) => {
error!("Failed to create the resolver. Error: {}\n", e);
std::process::exit(1)
} }
} }
resolver_ips
} }

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>,
@ -111,71 +101,62 @@ pub struct Service {
pub fn get_nmap_data( pub fn get_nmap_data(
filename: &str, filename: &str,
host: &str, host: &str,
min_rate: usize, min_rate: &str,
initial_port: usize, ports: &str,
last_port: usize,
fast_scan: bool, fast_scan: bool,
resolvers: &[Ipv4Addr],
) -> Result<Nmaprun, serde_xml_rs::Error> { ) -> Result<Nmaprun, serde_xml_rs::Error> {
let _ports_range = format!("{}-{}", initial_port, last_port);
let min_rate = min_rate.to_string(); let min_rate = min_rate.to_string();
let nmap_args = if fast_scan { let nmap_dns_resolvers = resolvers
vec![ .iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let mut nmap_args = vec![
"nmap", "nmap",
"-n",
"--dns-servers", "--dns-servers",
&NMAP_DNS_RESOLVERS, &nmap_dns_resolvers,
"-Pn", "-Pn",
"--host-timeout",
"5m",
"--min-rate",
&min_rate,
"-sS", "-sS",
"--open", "--open",
"-dd", "-dd",
"-T4", "-T4",
"--max-retries", "--max-retries",
"2", "3",
"-oX", "-oX",
filename, filename,
host, ];
]
if !min_rate.is_empty() {
nmap_args.append(&mut vec!["--min-rate", &min_rate]);
}
if fast_scan {
nmap_args.append(&mut vec!["--host-timeout", "20m"]);
} else { } else {
vec![ nmap_args.append(&mut vec!["-sV"]);
"nmap", }
"-n",
"--dns-servers", if !ports.is_empty() {
&NMAP_DNS_RESOLVERS, nmap_args.append(&mut vec!["-p", ports]);
"-Pn", }
"--host-timeout",
"10m", nmap_args.push(host);
"-sV",
"--min-rate", match Command::new("nmap").args(&nmap_args).output() {
&min_rate,
"-sS",
"-p-",
"--open",
"-dd",
"-T4",
"--max-retries",
"2",
"-oX",
filename,
host,
]
};
match Command::new("sudo").args(&nmap_args).output() {
Ok(_) => { Ok(_) => {
if Path::new(&filename).exists() && Path::new(&filename).is_file() { if Path::new(&filename).exists() && Path::new(&filename).is_file() {
serde_xml_rs::from_str(&std::fs::read_to_string(filename).unwrap_or_default()) serde_xml_rs::from_str(&std::fs::read_to_string(filename).unwrap_or_default())
} else { } else {
error!("Error executing nmap, you need root permissions. Leaving.\n"); error!("Error executing nmap, possible causes: Nmap is not installed or you need root/administrator permissions. Leaving.\n");
println!();
std::process::exit(1) std::process::exit(1)
} }
} }
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,55 +16,55 @@ use {
net::Ipv4Addr, net::Ipv4Addr,
time::Duration, time::Duration,
}, },
trust_dns_resolver::{config::ResolverOpts, proto::rr::RecordType},
}; };
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 async_resolver_all(args: &mut Args) -> Result<()> { pub fn parallel_resolver_all(args: &mut ProcessedArgs) -> Result<()> {
if args.with_output && !args.unique_output_flag && !files::check_full_path(&args.logs_dir) { if !files::check_full_path(&args.logs_dir) {
error!( error!("The logs directory {} does not exist.\n", args.logs_dir);
"Error creating the logs folder in the path {}. Leaving...",
&args.logs_dir
);
std::process::exit(1) std::process::exit(1)
} }
if !args.quiet_flag { if !args.quiet_flag {
info!( info!(
"Performing asynchronous 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 data = async_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(); let mut table = Table::new();
table.set_titles(row![ table.set_titles(row![
@ -72,8 +73,55 @@ pub fn async_resolver_all(args: &mut Args) -> Result<()> {
"OPEN PORTS", "OPEN PORTS",
"SERVICES" "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 { for (target, resolv_data) in &data {
if !resolv_data.ip.is_empty() { 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(); let mut services_table = Table::new();
for port_data in &resolv_data.ports_data { for port_data in &resolv_data.ports_data {
services_table services_table
@ -116,70 +164,80 @@ pub fn async_resolver_all(args: &mut Args) -> Result<()> {
]); ]);
} }
} }
}
if args.with_output && !args.targets.is_empty() { if args.with_output
if files::table_to_file(&table, files::return_output_file(&args)).is_err() && !args.targets.is_empty()
&& 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 {
table.printstd(); table.printstd();
} }
if ((args.with_output && !args.unique_output_flag) || args.unique_output_flag) if (args.with_output || args.unique_output_flag) && !args.quiet_flag {
&& !args.quiet_flag
{
info!( info!(
"Job finished in {} seconds.\n", "Job finished in {} seconds.\n",
args.time_wasted.elapsed().as_secs() args.time_wasted.elapsed().as_secs()
); );
info!("Logfile saved in {}\n\n", args.file_name); info!("Logfile saved in {}\n\n", args.file_name);
} }
if !args.quiet_flag {
println!();
}
Ok(()) Ok(())
} }
fn async_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<String, ResolvData> { fn parallel_resolver_engine(
let mut opts = ResolverOpts::default(); args: &ProcessedArgs,
opts.timeout = Duration::from_secs(2); targets: &HashSet<String>,
resolver: &hickory_resolver::TokioResolver,
) -> 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( resolv_data.ip = networking::get_records(resolver, &fqdn_target);
&networking::get_resolver(&RESOLVERS, &opts),
&fqdn_target,
RecordType::A,
);
(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", &ip); let filename = format!("{}/{}.xml", &args.logs_dir, &ip);
match nmap::get_nmap_data( match nmap::get_nmap_data(
&filename, &filename,
&ip, ip,
args.min_rate, &args.min_rate,
args.initial_port, &args.ports,
args.last_port,
args.fast_scan, args.fast_scan,
&resolvers,
) { ) {
Ok(nmap_data) => { Ok(nmap_data) => {
nmap_data nmap_data
@ -190,12 +248,9 @@ fn async_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<Strin
.unwrap_or_default() .unwrap_or_default()
.port .port
.retain(|f| f.state.state == "open"); .retain(|f| f.state.state == "open");
match std::fs::remove_file(&filename) { if args.no_keep_nmap_logs && std::fs::remove_file(&filename).is_err() {
Ok(_) => (), error!("Error removing filename {}.", &filename);
Err(e) => {
error!("Error removing filename {}. Description: {}", &filename, e)
} }
};
(ip.clone(), nmap_data) (ip.clone(), nmap_data)
} }
Err(e) => { Err(e) => {
@ -206,6 +261,11 @@ fn async_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<Strin
}) })
.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)| {
@ -232,3 +292,4 @@ fn async_resolver_engine(args: &Args, targets: HashSet<String>) -> HashMap<Strin
}) })
.collect() .collect()
} }
}

View file

@ -1,30 +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 initial_port: usize,
pub last_port: usize,
pub with_output: bool,
pub unique_output_flag: bool,
pub min_rate: usize,
pub from_file_flag: bool,
pub quiet_flag: bool,
pub custom_resolvers: bool,
pub custom_ports_range: bool,
pub fast_scan: 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 {
@ -32,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(),
} }

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.