Appearance
Docker
A small Alpine image bundles the full recon toolchain so you can run scans and generate reports without installing nmap, bash, jq, and the rest on the host, and so fleet hosts can pull a versioned image instead of building from source.
- Base:
alpine:3.21, single stage (netvuln-tool has no compiled dependencies, so there is nothing to build). - Contents: the whole source tree (
netvuln/ scripts/ lib/ templates/ assets/) plus the runtime tools listed below. The PythonnetvulnCLI drives the bash modules, which it resolves relative to the repo root, so a bare pip wheel would not work; the tree ships intact. - Entry point: the
netvulnCLI (recon,scan,upload,push,diff,status,daemon). - User: non-root (
netvuln).
Bundled tools
Required core: python3, bash, coreutils, gawk, grep, sed, jq, nmap + nmap-scripts (NSE).
Full recon: bind-tools (dig/host), whois, openssl, samba-client (smbclient), net-snmp-tools (snmpwalk/snmpget), curl, ca-certificates, tzdata.
Deliberately excluded: wkhtmltopdf (PDF export is broken on Alpine/musl; HTML reports are unaffected) and mail/sendmail (optional email delivery). If you need PDF export or email, run from a glibc-based host install instead (see Installation).
Build
From the repo root:
bash
docker build -t netvuln-tool:dev .On Apple Silicon / macOS, build for the deployment architecture (the fleet runs amd64):
bash
docker buildx build --platform linux/amd64 -t netvuln-tool:dev --load .Check the size:
bash
docker images netvuln-tool:devRun
Show CLI help (default command):
bash
docker run --rm netvuln-tool:dev --helpRun a recon scan. Scan sessions and generated reports land in /opt/netvuln-tool/sessions inside the container, so mount a volume to keep them:
bash
docker run --rm \
--network host \
-v "$PWD/out:/opt/netvuln-tool/sessions" \
netvuln-tool:dev recon -t 192.0.2.0/24--network host gives the container the host's network interfaces so it can see and scan the LAN. The default bridge network NATs the container onto its own subnet (for example 172.17.0.0/16), out of reach of your real targets, so for anything other than scanning the container host itself, use --network host.
Docker Desktop networking
--network host is a Linux Docker feature. On Docker Desktop (macOS/Windows) "host" is the Linux VM, not your laptop's LAN. Run scans from a Linux host (the fleet) for real LAN reach. Only scan networks and hosts you are authorized to assess.
Scan privileges: works out of the box (no --cap-add)
Privileged nmap phases (SYN scan, -O OS detection, -sU UDP for SNMP, and raw-socket host discovery) need raw sockets. This image runs as the non-root netvuln user, and there is a subtlety that trips up most containerized nmap setups:
Adding a capability to the container (
--cap-add=NET_RAW) puts it in the bounding set, but that is not effective for a non-root process. And nmap decides "am I privileged?" fromeuid==0, so it does not automatically use a capability even when one is present.
The image handles both halves so you do not have to:
- File capability on nmap:
setcap cap_net_raw+eip /usr/bin/nmap(baked at build time).NET_RAWis in Docker's default capability set, so the non-root user gets effective raw sockets with a plaindocker run, no--cap-addrequired. NMAP_PRIVILEGED=1(baked env): tells nmap to use raw scans despiteeuid != 0; the file capability makes that actually succeed.
Result: docker run --network host netvuln-tool:<v> recon -t <lan> performs full privileged scans as the non-root user, with no capability flags.
Capability caveats
- Do not
--cap-dropNET_RAW. The file capability carries the effective bit, so ifNET_RAWis removed from the bounding set the kernel refuses toexecnmap at all (Operation not permitted). Docker grantsNET_RAWby default, so this only bites if you deliberately drop it. If you must scan with all capabilities dropped, use a host install instead. - A few exotic nmap features (for example
--send-ethon some interfaces) also wantNET_ADMIN, which is not a Docker default. Add it only if you hit such a case:--cap-add=NET_ADMIN. It is intentionally not baked into the nmap file caps, as doing so would breakexecunder Docker's default cap set. --user rootremains the blunt fallback if capabilities are unavailable in your environment.
One command with Compose
docker-compose.yml wires up the frictionless path (network_mode: host, the sessions volume, and the image tag), so a scan is a single command:
bash
docker compose run --rm scan recon -t 192.168.1.0/24
# pin a version instead of :latest
IMAGE_TAG=v4.2.4 docker compose run --rm scan recon -t 10.0.0.0/24Reports and session data land in ./out. To build locally instead of pulling from the registry, uncomment build: . in the compose file.
Portal upload / license
Portal upload, push, CVE lookup, and license validation use curl over HTTPS. Pass the config and license via environment or a mounted config file, for example:
bash
docker run --rm \
-e NV_API_KEY="$NV_API_KEY" \
-v "$PWD/report_config.conf:/opt/netvuln-tool/report_config.conf:ro" \
netvuln-tool:dev upload -c report_config.conf --latestThe license soft gate reads ~/.netvuln/license.key (that is, /home/netvuln/.netvuln/license.key) or the NV_API_KEY / NV_LICENSE_KEY environment variables. It never blocks core scanning. See License Provisioning.
Bash entry points
The bash pipeline is still available directly if you prefer it over the Python CLI:
bash
docker run --rm netvuln-tool:dev bash scripts/vulnscan_recon.sh -t 192.0.2.10Smoke test
tests/docker_smoke.sh builds nothing; it exercises an already-built image and asserts the required tools are present and netvuln --help runs:
bash
tests/docker_smoke.sh netvuln-tool:devPublish to the Gitea container registry
Images are published to the in-house Gitea registry at 100.101.73.49:3000/Bullium-Consulting/netvuln-tool, which is already trusted by every fleet host's Docker daemon (insecure-registries).
Automated (CI)
.gitea/workflows/publish-image.yml builds and pushes automatically on:
- push to
main(tags:latestand:<VERSION>), - a
v*tag push (adds:<ref_name>, for example:v4.2.4), - manual
workflow_dispatch.
The version comes from the VERSION file. Consumers should pin to the :<VERSION> tag; :latest is published for convenience only.
One-time secret setup (operator action)
The auto-injected GITEA_TOKEN is rejected by Gitea's container registry, so the workflow authenticates with a dedicated personal access token:
- Create a Gitea PAT scoped
read:package+write:package. - In netvuln-tool -> Settings -> Actions -> Secrets, add:
REGISTRY_USER= your Gitea username (for examplewill)REGISTRY_TOKEN= the PAT from step 1
Without these, the docker login step fails.
Manual publish (validating the push path)
bash
docker login 100.101.73.49:3000 # PAT with write:package
VERSION="$(cat VERSION)"
IMG="100.101.73.49:3000/Bullium-Consulting/netvuln-tool"
docker buildx build --platform linux/amd64 -t "$IMG:$VERSION" -t "$IMG:latest" --push .Pull on a fleet host
bash
docker pull 100.101.73.49:3000/Bullium-Consulting/netvuln-tool:<VERSION>See also
- Installation: host install (
install.sh) and developer setup (setup.sh). - Raspberry Pi: running the container or a host install on ARM edge devices.
- License Provisioning: key formats, tiers, and the soft gate.
