Skip to content

Daemon Mode

scripts/netvuln_daemon.sh is a persistent scheduling daemon that replaces cron-based scan scheduling. Shipped in v3.0.0, it provides multi-config support, a built-in cron engine, retry with exponential backoff, portal heartbeats with system-info reporting, signal-driven lifecycle management, and soft deregistration handling. It is also the delivery vehicle for Command and Control commands, which ride back on heartbeat responses.

Architecture

The daemon is a long-lived process that continuously monitors one or more configuration files. Each *.conf file in the configs directory (default ~/netvuln-tool/configs/, or --configs-dir) defines a scan target, schedule, and portal credentials. On each poll the daemon evaluates cron expressions against the current time and launches scan pipelines when a schedule matches.

Config parsing applies a final overrides layer: _nvd_load_configs() re-applies <configs_dir>/.c2_overrides.conf after the *.conf files, so an allowlisted value pushed remotely wins over the file value and survives a reload. See Command and Control for the override mechanism.

Key Features

  • Multi-config support: every *.conf in the configs directory is loaded; each contributes independent targets, schedules, and credentials (the dotfile .c2_overrides.conf is deliberately excluded from the glob).
  • Built-in cron engine: parses standard cron expressions (SCAN_SCHEDULE) without the system cron daemon, evaluating minute, hour, day-of-month, month, and day-of-week fields.
  • Retry with exponential backoff: a failed scan is retried up to DAEMON_SCAN_RETRY_MAX times (default 3). Backoff is DAEMON_SCAN_RETRY_BACKOFF * 2^attempt seconds (default base 60), and the wait is interruptible so signals are still handled during backoff.
  • Portal heartbeats: periodic POST to the portal /api/health endpoint every DAEMON_HEARTBEAT_INTERVAL seconds (default 300), carrying agent id, version, status, uptime, schedule states, client id, a system-info snapshot, and the raw-scan capability self-check result (#215, see Privilege Model).
  • Signal-driven lifecycle: reload, graceful shutdown, and status dump without a restart.
  • Soft deregistration: an HTTP 403 heartbeat response triggers a clean shutdown.
  • Metadata injection: injects scheduled_by=daemon into session metadata, which the portal surfaces in its scheduled-scan dashboard.

Heartbeats and System Info

Each heartbeat cycle builds a JSON payload and POSTs it with the x-api-key header to ${api_url%/upload}/health (or DAEMON_HEARTBEAT_URL when set). The payload shape:

json
{
  "agent_id": "nvd_89f6e23d",
  "version": "4.3.3",
  "timestamp": "2026-07-18T12:00:00Z",
  "status": "running",
  "uptime_seconds": 3600,
  "schedules": [
    {
      "config": "acme_corp.conf",
      "schedule_id": "sched_ab12cd",
      "last_run": "2026-07-18T06:00:00Z",
      "last_status": "success",
      "next_run": "2026-07-19T06:00:00Z",
      "retry_count": 0
    }
  ],
  "client_id": "acme-corp",
  "system_info": { "os": {}, "hardware": {}, "packages": {}, "network": {}, "config": {} },
  "raw_scan_capable": true
}

The system_info block is gathered by _nvd_gather_system_info() and cached, refreshed only on the first heartbeat and then every 10th cycle to avoid overhead:

  • os: name, version, kernel, arch, hostname (reads /etc/os-release, falls back to sw_vers on macOS).
  • hardware: CPU count, total/available RAM (MB), total/available disk (GB) for /.
  • packages: detected versions of nmap, nikto, testssl, nuclei, whatweb, sslscan, and the netvuln-tool version.
  • network: non-loopback IPv4 addresses, public IP (via ifconfig.me), interfaces, DNS resolver, and a connectivity probe.
  • config: config path, log level, scan concurrency, storage path, and enabled modules.

The agent id is derived as nvd_<first 8 hex of md5(hostname)> and can be overridden with NVD_AGENT_ID (used in tests).

Soft Deregistration (HTTP 403)

Agents auto-register on their first heartbeat (no approval step). When an administrator deregisters an agent from the portal, the next heartbeat returns HTTP 403. The daemon logs Agent has been unregistered by administrator. Shutting down., sets the shutdown flag, and exits cleanly. The database record is retained (hidden from the fleet list) and can be re-enabled. See Command and Control for the full agent lifecycle (register, deregister, purge).

Signals

SignalBehavior
SIGHUPReload all config files. New configs are picked up, removed configs dropped, changed schedules take effect, and the persisted overrides file is re-applied.
SIGTERMGraceful shutdown. A running scan is allowed to finish before exit.
SIGINTGraceful shutdown (same as SIGTERM).
SIGUSR1Dump current status to ~/netvuln-tool/logs/daemon_status.json (pid, agent id, version, uptime, loaded configs, per-schedule next/last run and retry state, last heartbeat, raw-scan capability).

CLI Usage

Daemon

netvuln_daemon.sh [--configs-dir <dir>] [--foreground] [-h]

Requires DAEMON_ENABLED="true" in at least one config file. --foreground keeps the process attached instead of daemonizing (useful for debugging and under systemd).

Daemon Control

scripts/netvuln_daemon_ctl.sh is the management interface:

netvuln_daemon_ctl.sh <command> [options]

Commands:
  start              Start the daemon in the background
  stop               Stop the running daemon gracefully
  restart            Stop and then start the daemon
  reload             Send SIGHUP to reload configuration
  status             Show daemon status and schedule summary
  install-service    Install the systemd service unit (Linux only)
  uninstall-service  Remove the systemd service unit (Linux only)

The Python CLI also wraps this: python3 -m netvuln daemon <action> forwards to netvuln_daemon_ctl.sh. See Python Orchestrator.

Configuration

Daemon behavior is controlled through config-file variables:

VariableDefaultDescription
DAEMON_ENABLEDfalseEnable daemon scheduling for this config
DAEMON_POLL_INTERVAL60Seconds between schedule checks
DAEMON_HEARTBEAT_INTERVAL300Seconds between portal heartbeats
DAEMON_SCAN_RETRY_MAX3Max retry attempts on scan failure
DAEMON_SCAN_RETRY_BACKOFF60Base backoff seconds (doubles each retry)
DAEMON_LOG_LEVELinfoVerbosity: debug, info, warning, error

The six keys above (plus ALERT_ENABLED, ALERT_RISK_THRESHOLD, ALERT_ON_NEW_CRITICAL) are the allowlist that a portal push_config command may edit remotely; everything else (targets, schedules, storage path) is file-managed only.

systemd Integration

On Linux the daemon can run as a systemd service:

bash
# Install the service unit (renders templates/netvuln_daemon.service)
sudo ./scripts/netvuln_daemon_ctl.sh install-service

# Enable and start
sudo systemctl enable netvuln-daemon
sudo systemctl start netvuln-daemon

# Check status and follow logs
sudo systemctl status netvuln-daemon
journalctl -u netvuln-daemon -f

install-service writes the unit to /etc/systemd/system/netvuln-daemon.service and runs systemctl daemon-reload. uninstall-service removes it. Both refuse to run on macOS or where systemctl is absent.

bash
sudo ./scripts/netvuln_daemon_ctl.sh uninstall-service

Privilege Model

The daemon runs as the invoking non-root user (User=__USER__ in the rendered unit), never as root. Two nmap scan types need a raw socket and normally require root: OS fingerprinting (nmap -O, run automatically by discover_hosts.sh after a successful sweep) and SNMP/UDP enumeration (nmap -sU, used by enum_snmp.sh). Rather than run the whole daemon as root to get that, the systemd unit grants the single capability those scans need, the same rootless approach the Docker image already uses.

Two things happen together, both required:

  1. AmbientCapabilities=CAP_NET_RAW and CapabilityBoundingSet=CAP_NET_RAW in the unit. This gives the daemon process CAP_NET_RAW without giving it any other root-level capability. install-service also runs sudo setcap cap_net_raw+eip on the nmap binary itself as a second, belt-and-suspenders path; a file capability on nmap works even for a manually-run scan outside the daemon, and it is what lets a non-systemd invocation (a scan run by hand in a terminal) still use raw sockets. The setcap step is best-effort: if it fails, install-service warns and continues, because the unit's AmbientCapabilities is the durable mechanism and does not depend on the nmap binary's on-disk state.
  2. NMAP_PRIVILEGED=1 in the environment. nmap decides whether it is allowed to use raw sockets by checking EUID == 0; it does not inspect ambient capabilities or file capabilities on its own, so without this variable a non-root nmap still refuses privileged scan types even when the capability is present. The unit sets it via Environment="NMAP_PRIVILEGED=1". discover_hosts.sh and enum_snmp.sh also export it themselves, belt-and-suspenders, whenever they are about to run a privileged scan type non-root, so a manually-run scan with a setcap'd nmap works even outside the daemon's own environment.

nv_can_raw_scan() in lib/netvuln_common.sh is the single gate both discover_hosts.sh's OS-detection step and any future privileged-scan caller should use. It returns success when any of the following hold: the process is root; the process itself holds CAP_NET_RAW in its effective capability set (read from CapEff in /proc/self/status, the same ambient capability the systemd unit grants); or the process is not root but nmap carries the cap_net_raw file capability (checked via getcap, after resolving symlinks with readlink -f). The middle check is what lets the daemon's own scheduled scans keep working purely off the unit's AmbientCapabilities grant, independent of whatever state the nmap binary's file capability happens to be in. enum_snmp.sh's -sU call has no explicit gate; it simply works once the process has a capability and NMAP_PRIVILEGED=1, the same way it already worked as root.

nmap upgrades: prevention, resilience, and detection (#215)

A file capability set with setcap lives on the binary's inode, not in a database, so reinstalling or upgrading nmap through the package manager replaces the binary and silently drops the capability. Three layers now address that:

  • Resilience. Because nv_can_raw_scan() also checks the process's own ambient CAP_NET_RAW (see above), the daemon's own scheduled OS detection and SNMP/UDP scans keep working through an nmap upgrade even before anything reapplies the file capability. Only a manually-run scan outside the daemon, which has no ambient capability of its own, still depends on the file capability.
  • Prevention. install-service drops an idempotent apt post-invoke hook at /etc/apt/apt.conf.d/99-netvuln-nmap-cap that reapplies setcap cap_net_raw+eip on the resolved nmap path after every apt/dpkg run, so the file capability self-heals right after the upgrade that reset it, with no manual step. uninstall-service removes the hook. Like the setcap step itself, this is best-effort: a failure only warns, and the durable AmbientCapabilities grant and the resilience layer above are unaffected either way.
  • Detection. The daemon runs the same nv_can_raw_scan() check at startup and once per heartbeat cycle. A true to false transition, meaning the capability was actually lost, logs a loud WARNING to daemon.log/the journal, rather than the quiet nv_info line a skipped scan used to log on its own. The current state is exposed as raw_scan_capable (boolean) in both daemon_status.json and the portal heartbeat payload, so it is visible without grepping logs.

If the file capability is ever out of date on a host that predates the apt hook, re-run install-service, or sudo setcap cap_net_raw+eip $(command -v nmap) by hand.

Skipped-scan visibility (#256)

A per-run OS-detection skip, nv_can_raw_scan() returning false when discover_hosts.sh runs against a target, used to log only a quiet nv_info line: easy to miss unless someone was watching the live console, and nothing recorded it in the session results. discover_hosts.sh now calls nv_warning, naming nmap -O specifically, and records an "OS Detection Skipped" finding (severity info) via nv_json_add_finding(), so the gap also shows up in the report and logs. This is visibility-only: it changes nothing about when OS detection runs, still gated by the same nv_can_raw_scan() check, or about the rootless CAP_NET_RAW privilege model described above (#209).

Apache-2.0 licensed (appliance subtree proprietary)