A resumable, plugin-based network scanner for penetration testing engagements.
SmartScanner wraps nmap with a stateful, interactive shell so you can fire off
network sweeps, walk away, come back the next day, and pick up exactly where
you left off. Discovered hosts and ports are persisted in a local SQLite
database. As ports are discovered, protocol-specific plugins automatically run
against them (HTTP headers, SSL certificates, SMB shares, SSH algorithms, RDP
NLA, FTP anon, VNC, Telnet, screenshots, reverse DNS, ...). Everything can be
streamed live into a SysReptor project.
Unlike a one-shot nmap invocation, it:
- Keeps all state (network ranges, scan tasks, hosts, ports, plugin results) in a local SQLite database, so you can pause, exit, and resume scans across sessions.
- Splits large CIDR ranges into manageable chunks (max
/25) and runs them in parallel. - Performs a two-stage scan: a fast SYN sweep of the top 1000 TCP ports
to identify live hosts, then a full
-p- -sV -sC --script vuln,default,safe,discovery,authscan against the hosts that were actually responsive. - Runs plugins automatically against discovered services (e.g. takes a screenshot of every HTTP service, dumps every SSL cert, lists SMB shares, flags weak SSH ciphers, ...).
- Continuously syncs results to SysReptor as structured notes (one note per host, with screenshots inlined).
- Plugins
- Installation
- Quick Start
- Shell Commands
- Settings Reference
- Example: End-to-End Pentest Workflow with SysReptor
- More Examples
- Data Storage
- Building a Standalone Binary
Plugins are auto-loaded on startup and hook into port discovery. Each plugin declares which ports/services it cares about and only runs when a matching port is found.
| Plugin | Trigger | What it does |
|---|---|---|
dns_reverse |
Every new host | Reverse-DNS lookup for the host's IP |
ftp_anon |
FTP ports (21, etc.) | Tries anonymous login |
http_headers |
HTTP/HTTPS ports (80, 443, 8080, 8443, 8000, 8888, 3000, 5000, 9090, or service contains http) |
Fetches response headers, page <title>, and fingerprints common tech (WordPress, Jenkins, Grafana, Tomcat, IIS, ...) |
http_screenshot |
Same as http_headers plus 8843, 9443 |
Takes a headless Chrome screenshot of the IP and of every hostname found in the SSL cert SANs |
rdp_nla |
RDP (3389) | Checks whether Network Level Authentication is enforced |
smb_info |
SMB (445, 139) | NetBIOS name query, anonymous + authenticated SMB enumeration (OS, signing, shares, directory listing) |
ssh_info |
SSH (22) | Enumerates KEX / host-key / cipher / MAC algorithms and flags weak ones |
ssl_info |
TLS-speaking ports | Extracts CN, SANs, issuer, validity. Found hostnames feed back into http_headers and http_screenshot so they re-run per SAN. |
telnet_info |
Telnet (23) | Grabs the cleartext banner (and flags it as plaintext) |
vnc_info |
VNC (5900+) | Negotiates the RFB handshake and reports the supported auth types |
If a plugin's dependencies are missing (e.g. paramiko for ssh_info,
impacket for smb_info, Chrome for http_screenshot), it is disabled at
startup and you will see a Plugin disabled: ... message — the rest of
SmartScanner continues to work.
- Python 3.8+
nmap(must be in$PATH). On macOS:brew install nmap. On Debian/ Ubuntu:sudo apt install nmap. On Windows: install from https://nmap.org/download.html.- Chrome or Chromium (optional, only needed for the
http_screenshotplugin). On macOS just install Google Chrome normally; Selenium 4 will manage the driver automatically.
git clone <repo-url> smartscanner
cd smartscanner
# (optional) virtualenv
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtpython -m smartscanner.mainYou should see:
Initializing SmartScanner...
nmap found: /opt/homebrew/bin/nmap
Loaded plugin: dns_reverse
Loaded plugin: http_headers
Loaded plugin: http_screenshot
...
SmartScanner - Network Scanner
Type 'help' for commands.
SmartScanner>
Tip: SYN scans (
-sS) require root/Administrator. SmartScanner automatically falls back to-sT(TCP connect) ifnmapcomplains about privileges, so running unprivileged is fine — it's just a bit slower and more visible on the wire. For best results, run withsudo.
SmartScanner> set scan_speed aggressive
Scan speed set to 'aggressive' (nmap -T4)
SmartScanner> set max_parallel_tasks 8
Setting 'max_parallel_tasks' set to '8'
SmartScanner> add 10.0.0.0/24
Added network range: 10.0.0.0/24 (ID: 1)
SmartScanner> resume
Scanning resumed
SmartScanner> status
=== Scan Status ===
Networks: 1 total (0 pending, 1 scanning, 0 completed, 0 paused)
Tasks: 2 total (1 pending, 1 running, 0 completed, 0 failed)
Hosts: 0 total ...
When the basic scan finishes for a chunk, SmartScanner automatically queues a
full scan for the same chunk, and plugins fire as ports come in. You can
pause, quit, restart later, and resume — pending tasks are picked back
up from the database.
| Command | Description |
|---|---|
add <cidr> |
Add a network range to scan (e.g. add 192.168.1.0/24). Gets split into /25 chunks. |
status |
Show counts of networks / tasks / hosts / ports. |
tasks [all|pending|running|failed|completed] [N] |
List scan tasks. With no arg shows non-completed ones. |
task <id> |
Show full details of a single scan task, including error output. |
hosts |
List discovered hosts. |
ports <host_ip> |
List open ports for a host. |
plugins [executions] |
List loaded plugins and per-plugin execution summary. Add executions to see every execution row. |
retry [scan|plugin] [id] |
Re-queue failed tasks / plugin executions. With no arg retries everything that failed. |
rerun scan <id|all|basic|full> |
Force-rerun successful scan tasks. |
rerun <plugin_name> [port_id] |
Force-rerun a plugin (optionally on a specific port). |
rerun execution <id> |
Force-rerun a single plugin execution by ID. |
rerun all |
Force-rerun every completed plugin execution. |
pause |
Pause all scans (workers stop pulling new tasks; current ones finish). |
resume |
Resume paused / pending scans and plugin executions. |
clear [completed|failed|all] |
Delete finished task rows. Default = completed only. |
fixstatus |
Recalculate host statuses (discovered → basic_scan_done → full_scan_done → plugins_done) from actual scan data. |
settings |
Print all current settings. |
set <key> <value> |
Update a setting (see Settings Reference). |
export <json|csv|xml> [file] |
Dump results to disk. Default filename is smartscanner_export_<timestamp>.<ext>. |
sync [full] |
Push data to SysReptor. Default is a delta sync (only changed hosts); sync full re-pushes everything. |
quit / exit |
Shut down gracefully. |
Settings persist in the database. Set them with set <key> <value> and view
them with settings.
| Key | Type / Allowed values | Default | Meaning |
|---|---|---|---|
scan_speed |
paranoid / sneaky / polite / normal / aggressive / insane |
normal |
Maps to nmap -T0..-T5. |
nmap_min_rate |
int 0–10000 |
300 |
nmap --min-rate packets/sec. 0 disables. |
nmap_host_timeout |
int seconds, 0–86400 |
0 |
nmap --host-timeout per host (full scans only). 0 disables. |
nmap_script_timeout |
int seconds, 0–86400 |
0 |
nmap --script-timeout per NSE script. 0 disables. |
nmap_version_intensity |
int 0–9 |
0 |
nmap --version-intensity. 0 uses nmap's default (7). |
disable_ping |
true / false |
false |
Skip the basic top-1000 sweep, jump straight to the full scan with -Pn. |
chunk_size |
int 1–65536 |
256 |
Reserved; chunks are currently capped at /25 (≤128 hosts) by nmap_wrapper.py. |
max_parallel_tasks |
int 1–32 |
4 |
Number of concurrent nmap scan workers. |
max_parallel_plugins |
int 1–16 |
2 |
Number of concurrent plugin worker threads. |
plugin_timeout |
int seconds, 10–3600 |
300 |
Soft per-plugin timeout. |
auto_resume |
true / false |
true |
Reserved (no longer auto-runs at startup; use resume). |
| Key | Type | Default | Meaning |
|---|---|---|---|
smb_user |
str | |
Username for authenticated SMB enumeration. |
smb_password |
str | |
Password for authenticated SMB enumeration. |
smb_domain |
str | |
Domain / workgroup. Leave empty for local accounts. |
If left empty, smb_info will still try anonymous / null sessions and report
shares it can list that way.
| Key | Type / Allowed values | Default | Meaning |
|---|---|---|---|
reptor_server |
URL | |
e.g. https://reptor.example.com |
reptor_token |
str | |
SysReptor API token (see SysReptor → User → API Tokens). |
reptor_project_id |
UUID | |
The project ID to write notes into. |
reptor_background_sync |
true / false |
false |
When true, a control thread pushes changed hosts every ~30 seconds. |
reptor_background_sync_threads |
int 1–16 |
2 |
Number of subprocesses that perform background sync in parallel. |
reptor_sync_timeout |
int 60–36000 |
600 |
Hard timeout for a single sync subprocess invocation (seconds). |
This is the typical engagement workflow. The SysReptor side is set up once, then SmartScanner streams its findings into the project for you.
In your SysReptor instance:
- Create a new project for the engagement (or open an existing one).
- Note the project ID from the URL:
https://<sysreptor>/projects/<PROJECT_ID>/... - Create an API token: click your user → API Tokens → Create token → copy it (you'll only see it once).
The first sync will create a root note called
Smartscannerin that project, withHosts → Hosts - Open PortsandHosts → Hosts - No Open Portsas sub-notes. Each scanned host becomes its own note underneath, titled by IP, with an emoji indicating scan progress (❓ → 🔍 → 🧠 → ✅).
Start SmartScanner and set the SysReptor credentials:
SmartScanner> set reptor_server https://sysreptor.example.com
Setting 'reptor_server' set to 'https://sysreptor.example.com'
SmartScanner> set reptor_token sR_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Setting 'reptor_token' set to 'sR_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
SmartScanner> set reptor_project_id 8f3c2b1a-0d4e-4a1c-9b7e-6f9a2c1d3e4f
Setting 'reptor_project_id' set to '8f3c2b1a-0d4e-4a1c-9b7e-6f9a2c1d3e4f'
Verify with:
SmartScanner> settings
For a typical internal pentest where you want to be quick but not too noisy:
SmartScanner> set scan_speed aggressive
SmartScanner> set max_parallel_tasks 6
SmartScanner> set max_parallel_plugins 4
SmartScanner> set nmap_min_rate 500
SmartScanner> set nmap_host_timeout 1800
SmartScanner> set nmap_script_timeout 120
If the network drops ICMP and you know hosts are alive anyway:
SmartScanner> set disable_ping true
This skips the basic ping/top-1000 sweep and goes directly to the full
-Pn -sS -sV -sC --script vuln,default,safe,discovery,auth -p- scan.
If you have domain credentials and want SMB share enumeration to be authenticated:
SmartScanner> set smb_user pentest
SmartScanner> set smb_domain CORP
SmartScanner> set smb_password 'Sup3rSecret!'
SmartScanner> set reptor_background_sync true
SmartScanner> set reptor_background_sync_threads 3
A control thread now wakes up every ~30s, picks up every host whose
version is greater than its synced_version, and pushes it to SysReptor
via 3 worker subprocesses. Screenshots are uploaded to SysReptor's file
store and embedded inline in the host note.
Note: any content you write outside of the
<!-- SMARTSCANNER_START:... -->/<!-- SMARTSCANNER_END:... -->markers in a managed note is preserved across syncs. Anything inside the markers will be overwritten on the next sync.
SmartScanner> add 10.10.0.0/16
Added network range: 10.10.0.0/16 (ID: 1)
SmartScanner> add 192.168.50.0/24
Added network range: 192.168.50.0/24 (ID: 2)
SmartScanner> resume
Scanning resumed
You can now leave it running. Use status, tasks, hosts, and plugins
to keep an eye on progress.
SmartScanner> hosts
=== Discovered Hosts ===
IP Address Hostname Status Ports
----------------------------------------------------------------------
10.10.4.17 dc01.corp.local plugins_done 12
10.10.4.42 fileserver.corp.local full_scan_done 5
...
SmartScanner> ports 10.10.4.17
=== Ports for 10.10.4.17 ===
Port Protocol Service Version State
---------------------------------------------------------------------------
53 tcp domain - open
88 tcp kerberos-sec - open
389 tcp ldap - open
445 tcp microsoft-ds - open
...
Background sync runs continuously, but you can also force a sync:
SmartScanner> sync # delta sync (only changed hosts)
SmartScanner> sync full # full re-push of every host
SmartScanner> export json results.json
SmartScanner> export csv results.csv
SmartScanner> export xml results.xml # nmap-compatible XML
SmartScanner> set scan_speed sneaky
SmartScanner> set nmap_min_rate 0
SmartScanner> set max_parallel_tasks 2
SmartScanner> set max_parallel_plugins 1
SmartScanner> add 198.51.100.0/24
SmartScanner> resume
-T1 plus a single scan worker keeps packet rates very low.
SmartScanner> set scan_speed insane
SmartScanner> set nmap_min_rate 5000
SmartScanner> set max_parallel_tasks 16
SmartScanner> set max_parallel_plugins 8
SmartScanner> set disable_ping true
SmartScanner> add 172.16.0.0/22
SmartScanner> resume
You configured SMB credentials after the SMB hosts were already scanned.
Re-run smb_info on everything:
SmartScanner> set smb_user pentest
SmartScanner> set smb_password 'Sup3rSecret!'
SmartScanner> rerun smb_info
Or re-run it for one specific port (look up the port ID with
plugins executions):
SmartScanner> rerun smb_info 42
SmartScanner> tasks failed
SmartScanner> retry
The database lives in ~/.smartscanner/. To start completely fresh:
rm -rf ~/.smartscannerThis deletes the database, screenshots, and the Selenium driver cache.
| Path | Purpose |
|---|---|
~/.smartscanner/smartscanner.db |
SQLite DB (hosts, ports, tasks, plugin results, settings) |
~/.smartscanner/screenshots/ |
PNGs taken by http_screenshot |
~/.smartscanner/selenium-cache/ |
Selenium Manager's driver cache |
You can override the DB path with the SMARTSCANNER_DB_PATH environment
variable, e.g. one DB per engagement:
SMARTSCANNER_DB_PATH=/data/engagements/acme/scan.db python -m smartscanner.mainA PyInstaller spec is included. From the repo root:
pip install pyinstaller
pyinstaller smartscanner.spec
# → dist/smartscanner (single-file executable)The binary still requires nmap to be installed on the target machine, and
Chrome/Chromium if you want screenshots.