Appearance
Command and Control (C2)
Command and Control is the enterprise (msp tier) subsystem for managing your own fleet of authorized scanner agents from the consultation portal. An agent is a netvuln_daemon.sh process running on a scanner host that you control; C2 lets a portal administrator queue commands for one agent or a group, have the agent execute them, and report results back. It is authorized fleet management, not remote access to third-party systems, and it requires portal connectivity plus jq on the agent.
The agent side is lib/command_handler.sh; the transport is the daemon heartbeat channel described in Daemon Mode.
How Commands Reach an Agent
Commands ride back on the heartbeat response, so no inbound port or polling loop is needed on the agent:
Admin (portal dashboard / API) Agent (netvuln_daemon.sh)
------------------------------ --------------------------
POST /api/agent-commands ── queue ──▶ agent_commands (status = pending)
│
┌── heartbeat POST ───┘ (every DAEMON_HEARTBEAT_INTERVAL)
▼
response piggybacks pending commands (status → sent)
│
▼
nv_process_commands() executes each command
│
PATCH /api/agent-commands (status → acked, then completed / failed)
│
▼
command_results (exit_code, stdout, stderr, session_id)The heartbeat piggyback returns up to 20 commands per cycle; an explicit GET /api/agent-commands?agent_id=… poll returns up to 50. In normal daemon operation the piggyback is sufficient.
Requirements and Gating
- License tier:
msp.nv_process_commands()callsnv_require_tier c2; below msp, command processing is skipped as a soft gate (a warning, never an error). See License Provisioning. - Portal connectivity: the agent config must define
CONSULTATION_API_URLand an API key (NV_API_KEY, or the legacyCONSULTATION_API_KEY). jqmust be installed on the agent.
Agent Identity and Lifecycle
Identity. Each agent has an id of the form nvd_<first 8 hex of md5(hostname)> (for example nvd_89f6e23d), derived automatically. NVD_AGENT_ID overrides it (used in tests).
Registration. Agents auto-register on their first heartbeat. There is no approval step and no allowlist; the portal records the agent and emits an agent_first_seen audit event.
Deregistration (soft). An administrator can deregister an agent (portal PATCH /api/agents, action: deregister), which sets a deregisteredAt timestamp. On the agent's next heartbeat the portal returns HTTP 403; the daemon logs Agent has been unregistered by administrator and shuts down. The record is retained (hidden from the fleet list) and can be re-enabled with action: reenable.
Purge (hard). An already-deregistered agent can be permanently removed (portal DELETE /api/agents / action: purge), which deletes its heartbeat row, queued commands, results, and group memberships. Audit history is retained and an agent_purged event is appended.
Command Priority
priority is an integer 0-10 set when a command is queued. It is a portal-side dispatch-ordering hint only:
- Both the poll and the heartbeat piggyback select pending commands
ORDER BY priority DESC, created_at ASC, so within a dispatch batch a higher priority is handed over first, oldest-first as the tiebreak. - The agent does not read priority. Once commands reach the agent they run in the order received.
Use a higher priority to jump a command ahead of already-queued work for the same agent (for example a restart ahead of a backlog of scans).
Command Reference
All commands are queued with POST /api/agent-commands; payload is a JSON object. The source of truth for the keys each command reads is lib/command_handler.sh.
run_scan
Runs vulnscan_recon.sh on the agent.
| key | required | default | notes |
|---|---|---|---|
targets | yes | none | host(s)/CIDR(s), comma-separated. Empty fails with "No targets specified for run_scan". |
profile | no | full_recon | full_recon/full add --full; quick/quick_recon add -P quick_recon; any other value adds no mode flag. |
upload | no | false | when true, the resulting session (session JSON + pipeline log + report HTML + exec summary) is pushed to the portal after a successful scan, using the daemon's credentials. |
run_scan parses the session id it created from the scan output and reports it back, so the portal's Command History row links to the session detail.
push_session
Uploads an existing session on the agent to the portal on demand, using the daemon's credentials.
| key | required | default | notes |
|---|---|---|---|
session_id | no | latest | specific session id (YYYYMMDD_HHMMSS); defaults to the most recent session. The id is validated against the session-name pattern, so it cannot traverse out of the sessions tree. |
Use this when a run_scan was dispatched without upload: true, or to re-push a session.
update_schedule
Updates an in-memory schedule entry loaded from a config file.
| key | required | default | notes |
|---|---|---|---|
schedule_id | yes | none | must match a loaded schedule. |
action | no | update | update, create, or delete. |
cron | no | none | new cron expression. |
profile | no | none | new scan profile. |
targets | no | none | new targets. |
delete is acknowledged but does not remove the config file (that requires manual action).
push_config
Updates a single allowlisted daemon setting (see below).
| key | required | notes |
|---|---|---|
config_key | yes | must be in the allowlist. |
config_value | yes | the value to set (control characters are rejected). |
set_log_level
| key | required | accepted |
|---|---|---|
level | yes | debug, info, warning, error. |
restart
No payload. Signals the daemon to reload.
shutdown
| key | required | default | notes |
|---|---|---|---|
graceful | no | true | boolean; signals the daemon to shut down. |
update
Self-updates the agent from git (requires the agent to be a git checkout). On success the daemon reloads.
| key | required | default | notes |
|---|---|---|---|
branch | no | main | git branch to check out and pull. |
version | no | none | tag vX.Y.Z to check out (takes precedence over branch). |
Configuration Editing and Persistence
push_config (and the portal's Runtime Config editor) can change only an allowlist of safe keys:
DAEMON_POLL_INTERVAL DAEMON_HEARTBEAT_INTERVAL
DAEMON_SCAN_RETRY_MAX DAEMON_SCAN_RETRY_BACKOFF
DAEMON_LOG_LEVEL ALERT_ENABLED
ALERT_RISK_THRESHOLD ALERT_ON_NEW_CRITICALAny key outside this list is rejected ("Config key not in remote-update allowlist"). Scan targets, schedules, storage path, concurrency, and enabled modules are not remotely editable; they are managed in the config file.
A pushed change is applied to in-memory daemon state (live effect) and persisted so it survives a reload and a full restart:
push_configwrites each acceptedKEY=VALUEto<configs_dir>/.c2_overrides.conf, a dotfile that the*.confschedule glob never picks up, stored mode600and upserted (last write per key wins).- On reload,
_nvd_load_configs()re-applies that file as the final layer after the.conffiles, so a pushed override beats the config-file value. - The allowlist is re-validated on reload, and newline/CR characters are refused, so a tampered overrides file cannot smuggle in an arbitrary key or extra line.
To clear a pushed override, delete or edit .c2_overrides.conf on the agent and reload the daemon (SIGHUP).
Execution Details
- Timeout: each command runs under
NV_CMD_TIMEOUT(default 300 seconds). A command that exceeds it is killed and reportedfailed(exit 124). Afull_reconscan against a large range commonly exceeds 300s; run those on the host directly or raiseNV_CMD_TIMEOUT. - Output capture:
stdoutandstderrare truncated to 10 KB in the result. - Status flow:
pending→sent→acked→completedorfailed. Terminal states insert acommand_resultsrow. - Result reporting:
_nv_report_command_status()PATCHes the derived${api_url%/upload}/agent-commandsendpoint with thex-api-keyheader, sendingcommand_id,agent_id,status, and (when present)exit_code,stdout,stderr, andsession_id. Reporting is non-fatal.
Dispatching a Command
From the portal dashboard (recommended)
Admin dashboard → Command and Control → Dispatch: choose an agent (or group), pick a command, fill the guided form (or raw JSON payload), set a priority, and send.
From the API
Admin auth is a Netlify Identity JWT (not the agent API key).
bash
PORTAL="https://<portal-host>"
JWT="<Netlify Identity admin access token>"
curl -sS -X POST "$PORTAL/api/agent-commands" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"command_type": "run_scan",
"agent_id": "nvd_89f6e23d",
"payload": { "profile": "full_recon", "targets": "192.168.50.0/24" },
"priority": 0
}'Replace agent_id with group_target for a group broadcast. Track the result (admin, read-only) with the id from the POST response:
bash
curl -sS -H "Authorization: Bearer $JWT" "$PORTAL/api/agent-commands?command_id=1"Troubleshooting
- Agent never runs the command: confirm it is msp-tiered, not deregistered (a deregistered agent gets 403 and shuts down), and has portal connectivity. Commands sit
pendinguntil the next heartbeat. run_scanfailed at ~300s: the command timeout was hit. Usequick, a smaller target, run it on the host directly, or raiseNV_CMD_TIMEOUT.- A config change reverted after restart: confirm
.c2_overrides.confexists in the agent configs dir; persistence relies on it. - Command shows
completedbut nothing changed: check the returnedstdout/stderrin the History detail (truncated to 10 KB). - History Session ID has no link / 404s: the id only resolves once the session has been uploaded. Dispatch
run_scanwithupload: true, or send apush_sessioncommand.
Related
- Daemon Mode: the heartbeat transport and agent process.
- API Contract: the cross-repo wire contract for agent commands and results.
- License Provisioning: msp-tier gating.
