A modern web-based frontend for Nmap built with Go, Gin, GORM, Redis, and robfig/cron.
| Layer | Technology |
|---|---|
| Web Framework | Gin |
| Task Queue | Redis (BRPOP) |
| Scheduler | robfig/cron |
| Database | SQLite via GORM |
| Auth | JWT + bcrypt |
| Frontend | Go html/template + Tailwind CSS (built to a static file, no CDN) |
| Live Updates | Server-Sent Events (SSE) via Redis pub/sub |
| Nmap Execution | os/exec subprocess |
nmapwebui/
├── cmd/
│ ├── server/main.go # HTTP server entrypoint
│ └── worker/main.go # Background worker entrypoint
├── internal/
│ ├── config/ # Configuration (env vars)
│ ├── db/ # GORM database init + migrations
│ ├── models/ # GORM models
│ ├── middleware/ # JWT auth middleware
│ ├── handlers/ # HTTP handlers (auth, targets, scans, reports, admin, SSE)
│ ├── services/ # Business logic (nmap, locking, reports)
│ └── scheduler/ # Cron-based schedule checker
├── templates/ # Go html/template pages (base.html holds the shared shell)
├── static/
│ ├── css/input.css # Tailwind source; app.css is the committed build output
│ ├── js/common.js # Shared UI helpers (API wrapper, toasts, modals, pagination)
│ └── vendor/ # Chart.js and Font Awesome, vendored for offline use
├── tailwind.config.js # Tailwind content paths and theme
├── scripts/vendor.sh # Fetches pinned Chart.js / Font Awesome releases (dev only)
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Server + Worker + Redis
├── Makefile # Build/run shortcuts
└── go.mod
- Go 1.22+
- Redis
- Nmap installed on the system
- GCC (for SQLite CGO)
# 1. Copy environment config
cp .env.example .env
# 2. Start Redis
redis-server
# 3. Build and run the server
make run
# 4. In another terminal, start the worker
make workerThe server starts at http://localhost:51111 (override with PORT in .env). Log in with the superadmin credentials from .env. Both binaries load .env from the working directory automatically; real environment variables take precedence.
The UI has no runtime CDN dependency: Tailwind is compiled to static/css/app.css, and Chart.js and Font Awesome are vendored under static/vendor/. These outputs are committed, so running, building and the Docker image need only Go.
No Node is involved. Tailwind runs as its standalone binary, downloaded once into bin/ by the Makefile, and libraries are fetched with curl from pinned release URLs. Rebuild only after changing templates or static/css/input.css, or to upgrade a library:
make css # download Tailwind binary if missing, rebuild app.css
make css-watch # rebuild on every template change
make vendor # re-fetch Chart.js and Font Awesome (versions pinned in scripts/vendor.sh)
make frontend # vendor + css# Starts Redis, Web Server, and Worker
make deploy
# View logs
make logsAccess at http://localhost:51111.
| Command | Description |
|---|---|
make run / make worker |
Build and run the server / worker locally |
make frontend / make css |
Rebuild vendored assets and Tailwind CSS (standalone binary, no Node) |
make deploy |
Build with layer cache and restart (fast for code-only changes) |
make deploy-full |
Full rebuild without cache (needed when Dockerfile changes) |
make restart |
Restart containers without rebuilding |
make logs |
Tail container logs |
make down |
Stop containers, preserves database and reports |
make down-clean |
Stop containers and delete all data (volumes removed) |
The database (app.db) and scan reports are stored in a Docker volume (app_data).
This data persists across make deploy, make down, and container restarts.
To preserve data: always use make down (runs docker-compose down).
To reset everything: use make down-clean (runs docker-compose down -v).
Built-in profiles: quick_scan, intense_scan, ping_scan, service_scan, os_detection, comprehensive, plus custom argument support.
- Supports daily, weekly, monthly, and interval schedules
- Exactly-once execution via Redis distributed locks
- Schedule checker runs every 60 seconds
- Window-based deduplication prevents duplicate triggers
Two layers of locking prevent race conditions:
- Redis lock (
nmapwebui:lock:task:{task_id}) — ensures only one worker runs a given task at a time - DB state check —
queued/runningscan runs block new ad-hoc runs
Connect to /api/sse/scans/:run_id/events for real-time progress:
progressevents from nmap--stats-everyparsingstatusevents (starting, running, completed, failed, cancelled)- Heartbeat keepalive for connection stability
POST /api/scans/runs/:id/cancel stops a run. Queued runs are finalised immediately. For running scans the API sets a short-lived Redis flag (nmapwebui:scan:{run_id}:cancel); the worker polls it once a second, kills the nmap process and marks the run cancelled.
GET /api/reports/:id/diff compares a report with the previous report of the same task: hosts that appeared or disappeared, up/down status changes, and per-host ports that opened, closed or changed service/version. The report page renders this as a "Changes since previous scan" panel and marks new and closed ports inline.
GET /api/hosts aggregates the latest report of every task into one row per IP address: status, open ports, services, which tasks cover it, first and last seen. GET /api/hosts/:ip adds the port history across all reports. Open ports are tagged by a heuristic in internal/services/exposure.go ("high" for remote-admin, database, file-sharing and ICS services such as Telnet, RDP, SMB, Redis or Modbus; "medium" for services worth an inventory check such as SSH or alternate HTTP). It is a prioritisation aid, not a vulnerability rating.
Ctrl/⌘ K or / opens a palette that searches hosts, tasks, target groups, reports and runs (GET /api/search?q=) and lists navigation commands. g followed by d/h/a/t/r/p/s jumps to a section, n creates a task, d toggles compact table density and ? lists everything.
/settings lets each user change their email, the timezone used to evaluate schedules, and their password (GET/PUT /api/me). Last login is recorded on sign-in.
GET /api/health reports Redis reachability, the number of worker processes with a live heartbeat, and the queue depth. The UI header indicator polls it so a missing worker is visible immediately.
Nmap outputs saved as:
- XML (
-oX) — parsed into structured Host/Port findings - Normal text (
-oN) — raw output for download - PDF and HTML — generated on demand from the parsed findings. PDFs are drawn in pure Go with go-pdf/fpdf; no external renderer is required.
- JWT access tokens (cookie + Bearer header)
- Role-based access (
user/admin) - Password hashing with bcrypt
| Prefix | Description |
|---|---|
/api/auth |
Login, register, logout |
/api/targets |
CRUD for target groups & hosts (PUT /api/targets/:id replaces the target list) |
/api/scans |
Scan tasks & runs. Lists support q, scheduled, status, task_id filters; POST .../runs/:id/cancel stops a run |
/api/schedules |
Enable/disable scheduled scanning |
/api/reports |
List (filter by task_id, from, to, q), view, diff, download XML/TXT/CSV/PDF |
/api/hosts |
Host inventory from the latest report per task; /api/hosts/:ip for detail and history |
/api/search |
Global search for the command palette |
/api/me |
Own profile: email, timezone, password |
/api/health |
Redis, worker heartbeat and queue depth |
/api/admin |
Admin stats & user listing |
/api/sse |
Server-Sent Events for live scan progress |
Set via .env file or environment variables:
SECRET_KEY=change-me-in-production
DATABASE_URL=instance/app.db
REDIS_URL=redis://localhost:6379/0
NMAP_REPORTS_DIR=instance/reports
DEBUG=false
ACCESS_TOKEN_EXPIRE_MINUTES=120
NMAP_WORKER_POOL_SIZE=2- Nmap arguments are passed as a list to
exec.Commandto avoid shell injection - Custom arguments should still be validated at the UI layer
- Passwordless sudo may be required for privileged scans (
-O,-sS, etc.) - The Docker containers use
NET_RAWandNET_ADMINcapabilities for raw packet scans
MIT