Skip to content

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 Python netvuln CLI 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 netvuln CLI (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:dev

Run

Show CLI help (default command):

bash
docker run --rm netvuln-tool:dev --help

Run 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?" from euid==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:

  1. File capability on nmap: setcap cap_net_raw+eip /usr/bin/nmap (baked at build time). NET_RAW is in Docker's default capability set, so the non-root user gets effective raw sockets with a plain docker run, no --cap-add required.
  2. NMAP_PRIVILEGED=1 (baked env): tells nmap to use raw scans despite euid != 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-drop NET_RAW. The file capability carries the effective bit, so if NET_RAW is removed from the bounding set the kernel refuses to exec nmap at all (Operation not permitted). Docker grants NET_RAW by 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-eth on some interfaces) also want NET_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 break exec under Docker's default cap set.
  • --user root remains 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/24

Reports 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 --latest

The 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.10

Smoke 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:dev

Publish 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 :latest and :<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:

  1. Create a Gitea PAT scoped read:package + write:package.
  2. In netvuln-tool -> Settings -> Actions -> Secrets, add:
    • REGISTRY_USER = your Gitea username (for example will)
    • 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

Apache-2.0 licensed (appliance subtree proprietary)