Network scanning and monitoring appliance for macOS and Ubuntu, powered by Nmap, with a cross-platform web UI.
The product is a Python Flask application (app.py + the nmapui/ package)
running directly on the scanner host and serving a browser UI. macOS has an
optional menu-bar wrapper and LaunchDaemon for unattended operation. Ubuntu is
also a target scanner host; an installer and systemd service candidate now
exist, but live privileged-host validation is still required. See
deployment status.
The unattended installer is fail-closed until its host prerequisites pass. Provision Python 3.11+ and scanner/report tools from a root-owned, non-writable toolchain; on macOS, linked non-system libraries and Nmap's NSE/data directory must meet the same rule. Homebrew tools in a user-owned prefix are suitable for development, not for the root service or privileged helper. Then, from the repository root:
git clone https://github.com/techmore/TM-NmapUI.git
cd TM-NmapUI
/path/to/root-owned/python3 -m venv .venv # replace with your protected Python 3.11+
.venv/bin/python -m pip install -r requirements.txt
packaging/macos/install-daemon.sh --dry-run
sudo packaging/macos/install-daemon.shinstall-daemon.sh installs a system LaunchDaemon and provisions credentials
for optional remote access. It stages a root-owned runtime release and serves
Socket.IO with one threaded Gunicorn worker. The local browser opens without a
username or password. To inspect the generated credentials for optional remote
access:
sudo cat "/Library/Application Support/NmapUI/credentials.env"Root mode is the default, but it requires both a protected base Python and a
protected service toolchain. The --user <name> mode still requires the same
scanner-tool provenance because its helper runs Nmap as root. This host's
user-owned Homebrew Python, scanner binaries and NSE data fail the gate; no service has
been installed here.
Open http://127.0.0.1:9000 on the scanner host. Loopback access is trusted by default; remote browser clients still need configured credentials.
Validate the daemon without changing anything:
packaging/macos/install-daemon.sh --dry-runFor Ubuntu host setup, see the setup guide.
./install.sh --no-daemon
./start.sh # runs .venv/bin/python app.pyOn Ubuntu, use the Ubuntu service setup instead of the macOS Homebrew installer.
Local access does not require credentials. Set them only when enabling remote browser or API access:
export NMAPUI_USERNAME=admin
export NMAPUI_PASSWORD='choose-something-strong'
./start.sh| Layer | Technology |
|---|---|
| Runtime | Python 3.11+ |
| Web framework | Flask + Flask-SocketIO |
| Real-time | Socket.IO |
| Scanner | Nmap + NSE (Nmap Scripting Engine) |
| PDF generation | Playwright Chromium; optional wkhtmltopdf or WeasyPrint fallbacks |
| XML processing | xml.etree.ElementTree + xsltproc |
| Scheduler | In-process scheduler thread with a cross-process flock lock |
| Persistence | SQLite (WAL) plus JSON artifacts |
| macOS shell | Swift menu-bar wrapper (optional launcher) |
- Python 3.11 or newer
- nmap (with an updated script database)
- xsltproc (Homebrew
libxslton macOS; Ubuntu package on Linux) - Playwright Chromium for production PDF output; optional
wkhtmltopdfor WeasyPrint fallbacks when installed - macOS or Ubuntu as the intended scanner host; the Swift menu-bar wrapper and LaunchDaemon are macOS-only
install.sh installs Homebrew tools for macOS development. It does not
provision a protected production toolchain. Ubuntu's host toolchain must be
prepared before running its service installer; see the setup guide.
- Quick Scan — fast discovery of live hosts
- Complete Scan — full port/service scan with OS detection and vulners CVEs
- Dragnet Scan — rescan every host from a previous discovery
- VPN Helper — batched Phase 2 for remote, high-latency or very large scopes
(see
docs/vpn-helper-design.md)
Reports include host/IP/MAC/vendor, open ports and service versions, detected CVEs with CVSS scores, and a network topology fingerprint. HTML and PDF are written under the data directory.
Complete scans can query Vulners.com over HTTPS for CVE matches. The bundled
NSE script sends detected software/CPE and version details (plus the query
type), not the scanned IP address or hostname; Vulners can still observe the
scanner's outbound public IP and learns part of the internal software
inventory. Managed service installs disable this enrichment by default; an
operator must set NMAPUI_ENABLE_VULNERS=true in the protected service
credentials file and restart the service to enable it. The report UI clarifies
that no Vulners results do not prove a host is vulnerability-free. Quick
discovery scans do not invoke the Vulners script. Direct/manual app starts
retain the legacy behavior, with optional enrichment enabled unless an
individual feature flag is explicitly set to false.
The topology fingerprint uses outbound network services: a traceroute to
1.1.1.1 and an HTTPS public-IP lookup at api.ipify.org, which learns the
scanner's public IP. Managed service installs disable both by default. To opt
in, set NMAPUI_ENABLE_NETWORK_FINGERPRINT=true in the protected service
credentials file and restart the service. Local IP, subnet mask and CIDR
discovery remain available without outbound lookups. Inter and Instrument
Serif are self-hosted, so the app and generated scan reports do not contact
Google Fonts.
Update metadata is checked only after an operator clicks Check for updates
or an authorized Daedalus command requests it; opening the dashboard does not
contact GitHub. Managed service installs keep background update checks disabled
by default. NMAPUI_ENABLE_UPDATE_CHECK=true in the protected service
credentials file opts into background checks; an explicit operator check remains
available. Successful results are cached for six hours per process (one minute
after a failed check). The request reveals the scanner's outbound public IP but
does not send scan targets or results.
PDF rendering disables JavaScript and blocks external network resources in Playwright; supported fallbacks also disable active scripting and restrict resource access.
Auto-scan runs inside a daily window; Auto-Monitor runs per-customer rules daily,
weekly, biweekly, monthly or quarterly. Due-ness is derived from the persisted
last_run versus the most recent scheduled slot, so a run missed while the
scanner host slept or was powered off is picked up on the next tick. A failed
report leaves the slot due for a later retry. Scheduled reports run serially
in a worker so a long report does not stop the scheduler clock.
Auto-scan settings are written atomically; failed writes are reported instead
of acknowledged as saved. Queued auto-scan and Auto-Monitor work rechecks the
current rules before it starts, so disabling a rule cancels a pending launch.
Auto-Monitor last_run and rule creation times are server-owned; a stale
settings form cannot roll them back and trigger duplicate catch-up scans.
Auto-Monitor defaults accept an IANA time zone such as America/New_York.
When set, nonexistent spring-forward times move to the corresponding time
after the clock jump; ambiguous fall-back times use the first occurrence.
Leaving the field blank keeps the scanner host's local-clock behavior.
The service admits at most two active scan/report jobs by default. Set
NMAPUI_MAX_CONCURRENT_JOBS to a positive integer to change the limit;
requests above the limit receive a capacity message and can be retried.
Automatic database maintenance runs daily, keeping the newest 5,000 runtime
logs, 2,000 customer history rows and 2,000 finished jobs, and removing events
belonging to pruned jobs. Operators can override those positive-integer limits
in the protected service environment with NMAPUI_RUNTIME_LOGS_KEEP_LATEST,
NMAPUI_CUSTOMER_HISTORY_KEEP_LATEST and NMAPUI_FINISHED_JOBS_KEEP_LATEST;
the installers validate the values. Daily pruning does not run VACUUM on the
scheduler thread; use the authenticated Prune Logs + Compact DB action when
an explicit database compaction is appropriate. Neither operation deletes
saved reports or scan folders.
- Loopback browser and API access is trusted by default; no sign-in prompt is shown on the scanner host.
- Non-loopback browser access requires
NMAPUI_USERNAME/NMAPUI_PASSWORD; browsers sign in at/login, and API clients can use HTTP Basic auth. - Changing the configured username or password invalidates existing browser sessions; users must sign in again. Upgrading from an earlier build also requires one sign-in because its sessions were not bound to credentials.
- Empty credentials and the built-in default password are rejected for remote
authentication.
NMAPUI_ALLOW_DEFAULT_CREDENTIALS=trueonly acknowledges that password when local UI trust is enabled and the listener is loopback; do not use it with a reverse proxy or network-facing bind. - The server binds to loopback by default. Remote browser access requires an
explicit
NMAPUI_HOSTandNMAPUI_ALLOWED_ORIGINSconfiguration, valid sign-in, and a trusted VPN or HTTPS terminated by a reverse proxy. Socket.IO checks the same origin allowlist and the authenticated session. NMAPUI_TRUST_LOCAL_UI=falsealso requires sign-in for loopback callers. With the default setting, any local process can reach the scanner control surface; keep the service loopback-bound unless remote access is deliberately configured.- If a reverse proxy forwards remote clients to the loopback listener, set
NMAPUI_TRUST_LOCAL_UI=falsebefore exposing it. Otherwise the backend sees the proxy's local connection and may treat remote callers as trusted locals. For an HTTPS proxy, setNMAPUI_COOKIE_SECURE=trueso browser sessions are never sent over an accidental plain-HTTP access path.
The appliance is configured to run as root by default for full SYN/OS/ARP capability, subject to the Python and scanner-toolchain provenance checks.
sudo packaging/macos/install-daemon.shinstalls the daemon as root. No sudoers entry is written, because none is needed. Root mode requires a root-owned, non-writable base Python interpreter.- The backend binds loopback by default. Loopback access has no separate username/password prompt; any process running on the machine can reach the control surface. Remote callers still require credentials.
--user <name>opts into least privilege instead: the backend runs as that account and sudoers grantsNOPASSWDfor one validating helper only (/usr/local/libexec/nmapui-privileged-scanner), never fornmaporarp-scandirectly. That helper re-validates the program, the scan technique, every flag, the target, and any--script/--stylesheet/output path.- In both modes the installer stages root-owned copies of
vulners.nseand the stylesheets with each release under/usr/local/lib/nmapui/current/privileged-assets, so a writable checkout cannot inject Lua into a root-run nmap. The helper requires those trusted paths. Existing shared assets under/usr/local/share/nmapuiremain only as a fallback when rolling back to a release installed before this layout. - If the helper is absent (development), the app falls back to
sudo -n nmapand then to an unprivileged-sTscan, always surfacing the fallback.
Export the runtime database from the Settings tab, or download it directly:
curl -OJ -u admin:"$NMAPUI_PASSWORD" http://127.0.0.1:9000/api/runtime/exportHealth and readiness:
curl http://127.0.0.1:9000/api/health/live
curl http://127.0.0.1:9000/api/health/readyReadiness requires a live scheduler thread. The supervised worker also verifies its persistent session-signing key before startup; a corrupt or unwritable key fails the worker rather than silently rotating sessions. Session, Drive and remote-sync secret files are owner-only. Back up each encrypted token together with its matching key: a missing key is not regenerated while reading existing ciphertext. Startup recovery also has to finish cleanly for readiness to pass; failed interrupted-job updates leave the service degraded until the underlying storage issue is resolved and the worker is restarted.
Daemon lifecycle:
sudo launchctl kickstart -k system/com.techmore.nmapui # restart
sudo launchctl print system/com.techmore.nmapui # status
sudo packaging/macos/install-daemon.sh --uninstall # removeLogs live in /Library/Logs/NmapUI/ (server.out.log, server.err.log, and
the rotated application log).
Backfill scan artifacts into the runtime store:
.venv/bin/python scripts/backfill_runtime_store.pyscripts/nightly_product_eval.sh --runBoots the Flask app on port 9000, probes liveness, identity, the UI and a static
asset, and writes a JSON report under docs/notes/eval-logs/.
./build.shCompiles the Swift menu-bar wrapper, bundles a virtualenv with the Python
resources, and installs NmapUI.app. The wrapper attaches to the daemon when it
is running; it never requests administrator privileges.
Build environment variables:
NMAPUI_SWIFT_TARGET— override the Swift target (defaults fromuname -m)NMAPUI_APPLICATIONS_DIR— install destination (/Applicationswhen writable, otherwise~/Applications)NMAPUI_SKIP_OPEN=1— build without launchingNMAPUI_MIGRATE_DB=1 ./build.sh— migrate an existing runtime database during install;NMAPUI_MIGRATE_DB_FROM=<path>selects the explicit source database. The build takes a consistent SQLite backup, including committed WAL data. If the source is already the configured runtime database, no copy is needed. Stop any other process using the destination before migrating from a different source, and retain a separate pre-upgrade backup for rollback.
The scanner-host targets are macOS and Ubuntu. Each host runs the Flask backend
directly with access to its network interfaces, and users can connect through a
browser on another platform. macOS currently has the bundled menu-bar launcher
and LaunchDaemon installer. Ubuntu now has a root-owned installer and
systemd unit. A disposable Ubuntu 24.04 ARM64 VM validated the installed
service, authenticated browser/Socket.IO, privileged loopback SYN scanning,
reboot/crash recovery, upgrades, and failed-upgrade/rollback recovery. A real
scanner host must still validate its physical interface and authorized subnet
scan paths, followed by an unattended soak; Ubuntu is not yet production
ready. Track that remaining gate in the Ubuntu deployment
issue.
Windows is not a scanner-host target in the current plan.
Container packaging is retired from the active roadmap (September 11, 2026).
The retained Dockerfile and docker-compose.yml run the legacy Node runtime
and are historical references, not supported installation paths.
/— stable entrypoints:app.py,start.sh,install.sh,build.shnmapui/— application package (scanning, reporting, scheduling, runtime DB)nmapui/handlers/— HTTP and Socket.IO route registrationtests/— pytest suite, including browser regressions and a packaged smoke testpackaging/macos/— Swift menu-bar wrapper andinstall-daemon.shpackaging/ubuntu/— systemd service installer candidatepackaging/pyinstaller/— alternative standalone bundle specdocs/guides/,docs/notes/,docs/audits/— guides, notes and auditsserver.js,index.html,static/js/,package.json— legacy Node/Express build kept only as a rollback reference (releases/TM-NmapUI-mac-known-good.md)
Runtime state (data/, *.sqlite3, logs/, config/customers.yaml, eval logs
and app bundles) is gitignored and must stay out of version control.
MIT