Security probes

Axiom Border assesses the network it sits on with four probes. They share one execution model, one audit trail and one accumulated view of results, so the findings of each one reinforce the others instead of living in separate reports.

Probe What it answers Requires
Network discovery What is out there, and what is it listening on? nmap 7.80+ on PATH
Vulnerability scanning Which of those services are vulnerable? A templates directory (OT/ICS checks are included)
SNMP What does the device say about itself? Reachable SNMP agent, credentials or a profile
Traffic capture What is actually crossing the wire? libpcap, a valid capture interface, root

Each runs two ways: automatically on a configurable interval, and manually on demand from the API or the web UI. Results are kept locally as probe state and written to InfluxDB as metrics, and every execution is audited.

flowchart TB
    DISC["Network discovery<br><i>live hosts and ports</i>"]
    DISC --> VULN["Vulnerabilities"]
    DISC --> SNMP["SNMP identity"]
    DISC --> CAP["Traffic capture"]
    VULN --> INV
    SNMP --> INV
    CAP --> INV
    DISC --> INV[("Consolidated<br>inventory")]
    INV --> ALARM["Alarms and<br>audit trail"]

Network discovery establishes the baseline and the other three enrich it. That ordering matters: a vulnerability scan with no targets scans every host in the baseline, so a discovery scan must have run first for it to have anything to do. Newly discovered hosts also trigger a vulnerability scan on their own, grouped together after a short delay, so new assets get assessed without waiting for the next interval.

Where you see the results

Network status in the console is the operational view of everything on this page: the discovered hosts with their addresses, when each was first and last seen, and per-host actions for its open ports, its vulnerabilities and its SNMP data. The charts below summarise findings by severity, hosts by status, and how many hosts carry vulnerabilities at all.

The Network status view, listing discovered hosts with their ports and vulnerabilities

The header names the last assessment that ran and how it finished, so you can tell at a glance whether what you are looking at is current.

Each row carries its own actions. Scan opens the Ports panel for that host — what it is listening on, with the service the scan identified and when the port was first and last seen:

The Ports panel for a single host

Vulnerabilities opens the findings for that host. Each row names the check that fired and explains what it confirmed, which is what makes a finding actionable rather than just a label:

The Vulnerabilities panel, listing the checks that fired against one host

Both panels carry a re-scan button, so you can reassess a single host without launching a sweep of the whole range.

How to read a finding

Name says what was confirmed — Modbus/TCP Diagnostics Function Exposed Without Authentication — and Description explains how it was confirmed and why it matters. Two more columns sit to the right of the description: CVE, empty whenever the finding is an exposure rather than a published vulnerability, and Template ID, which always identifies the check (modbus-diagnostics-exposure and the like). Since not every finding has a CVE, the template identifier is the stable way to refer to one.

Checks from the write/control layer are marked twice over: the name ends in [INTRUSIVE] and the description opens with INTRUSIVE / GATED. Those only ever run when a scan explicitly asks for intrusive mode — see OT/ICS vulnerability scanning.

The shared execution model

Every probe launch is asynchronous: the request is accepted, you get an identifier back immediately, and the work continues in the background.

Open Network status and use Edit to set the scan parameters, then Reload to refresh the view once the run finishes. The header keeps showing the state of the most recent execution while it progresses.

curl -s -X POST http://192.168.1.10:8083/security/scan/network \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"targets": ["192.168.1.0/24"], "service": true}'
{ "uuid": "4a79214d-3deb-4ae8-933f-9874cbdc1128", "status": "inprogress" }

Poll GET /security/executions/{uuid} until the status leaves inprogress, landing on finished or failed. An execution record carries two payloads that are easy to confuse: currentOutput is the result of that run alone, while currentStatus is the consolidated view of the affected hosts. To list completed runs, filter by finished.

Executions also publish over MQTT

If you would rather not poll, the embedded broker publishes started, finishedok and failed events for every execution. See MQTT and OpenGate operations.

Network discovery and port scanning

The discovery probe. It finds live hosts, enumerates ports, and optionally identifies services and operating systems.

curl -s -X POST http://192.168.1.10:8083/security/scan/network \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "targets": ["192.168.1.0/24"],
        "portFilter": "1-1024,502,2404,20000,47808",
        "timing": "T3",
        "service": true,
        "os": false,
        "timeout": "30m"
      }'

The portFilter above is worth copying if you have industrial equipment: it covers the common IT range plus the Modbus, IEC-104, DNP3 and BACnet ports, so the OT assets show up in the baseline that the vulnerability probe will later work from.

Practical notes:

  • os: true needs root, because OS fingerprinting implies a SYN scan over raw sockets. Without it, leave OS detection off.
  • udp: true is slow. Restrict udpPorts rather than sweeping.
  • Only one discovery scan runs at a time. A second request returns 409 Conflict unless you pass ?force=true, which cancels the running one and marks it failed with cancelled due to forced run.
  • The default portFilter is the full 1-65535 range. On a /24 at cautious timing that is a long scan — narrow it.

Scheduled behaviour is configured under securityProbes.nmap — from the console’s Configuration view, or in the file. See Configuration.

Vulnerability scanning

Template-based scanning, in two distinct halves. The web and CVE half uses the standard upstream template tree and needs that tree present on disk. The OT/ICS half uses Axiom Border’s own suite of 57 industrial protocol checks, which are installed with the product and work with no internet access.

curl -s -X POST http://192.168.1.10:8083/security/scan/vulns \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"targets": ["192.168.1.50"], "level": "medio", "severity": "high,critical"}'

Depth is chosen with level, which takes one of four values — ligero, medio, profundo or ot. Always use one of those four: each selects a different set of checks, and the mapping table in Configuration is worth reading before you pick one.

Industrial protocol scanning has its own page, because the safety model deserves the space:

OT/ICS vulnerability scanning — Modbus/TCP, IEC 60870-5-104, DNP3, BACnet/IP and OPC UA, with the two-lock model that governs write operations.

Check the templates directory before trusting an empty report

A web scan that reports nothing looks exactly like a web scan that had no templates to run. Confirm the directory is populated:

ls /opt/axiom-border/vulnscan-templates/

You should see per-protocol directories such as http/, network/ and ssl/. CVE templates live under http/cves/, not at the root. OT/ICS findings are unaffected, because those checks are installed with the product.

SNMP — interrogation and profiles

Two operations: a GET of specific OIDs, and a walk from a root OID.

Symbolic names work as well as numeric OIDs — sysDescr is resolved through the bundled MIB catalogue, which matters on an air-gapped probe where you cannot look an OID up.

curl -s -X POST http://192.168.1.10:8083/security/scan/snmp \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "target": "192.168.1.50",
        "oids": ["sysDescr", "sysName", "sysObjectID"],
        "timeout": "15s"
      }'

Profiles and associations

Rather than passing credentials with every request, store them once as a profile and bind them to hosts with an association:

# Create a v3 profile
curl -s -X POST http://192.168.1.10:8083/security/snmp/profiles \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "plc-floor-v3",
        "version": "3",
        "username": "<snmp-user>",
        "securityLevel": "authPriv",
        "authProtocol": "SHA",
        "authPass": "<auth-passphrase>",
        "privProtocol": "AES",
        "privPass": "<priv-passphrase>"
      }'

# Bind it to a host
curl -s -X POST http://192.168.1.10:8083/security/snmp/associations \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"ipAddress": "192.168.1.50", "profileName": "plc-floor-v3"}'

Resolution order when a scan runs: the request’s profileName wins, then the per-IP association, then the configured default. A target that resolves to no profile fails with snmp omitted targets due to missing profile: <ip>.

Supported versions are v1, v2c and v3. For v3, defaults when fields are left empty are authPriv with SHA and AES.

Prefer profiles over inline credentials

Credentials sent inline as customParams are persisted with the execution record and returned in its detail. Profiles keep them out of execution history. Use customParams for one-off diagnostics only.

Note

The scheduled SNMP probe runs a discovery pass first, with a fixed budget of 30 seconds. A /24 at cautious timing will exhaust it, so narrow the range or raise the discovery timing value.

Traffic capture

Two modes, both capturing IPv4 traffic through libpcap:

  • Continuous, driven by configuration. When securityProbes.sniffing.interfaces is non-empty, capture starts automatically and persists flow counters to the networktraffic bucket every flushInterval.
  • On demand, via the API, for a bounded duration, merged into the latest scan result.
curl -s -X POST http://192.168.1.10:8083/security/scan/sniff \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "interfaces": ["eth0"],
        "bpf": "tcp port 502",
        "duration": "5m",
        "promiscuous": true
      }'

The BPF filter above captures Modbus traffic only, which is a good way to confirm an industrial segment is actually active before scanning it.

Capture contributes bytesIn/bytesOut and packetsIn/packetsOut counters plus samples to each host in the merged result.

The interface name is the usual culprit

Capture failing silently almost always comes down to the interface. It must be a pcap name: eth0 or enp3s0 on Linux, en0 on macOS, and an Npcap NPF device path such as \\Device\\NPF_{GUID} on Windows — the friendly name Ethernet does not work there. Confirm with nmap --iflist.

Also confirm the service runs as root, and that there is traffic to see: sudo tcpdump -i <iface> -c 10.

The merged result view

Individual executions tell you what one probe found. GET /security/results/last tells you what is known about a host, merged across all four probes:

curl -s 'http://192.168.1.10:8083/security/results/last?ipAddress=192.168.1.50' \
  -H "Authorization: Bearer <jwt-token>"

Each host carries ipAddress, hostname, macAddress, os, manufacturer, status, firstSeen and updatedAt, then the per-probe contributions: ports from discovery, vulnerabilities from the vulnerability scan, snmp entries from the SNMP probe, and sniffing counters from traffic capture. This is the view worth putting on a dashboard.

The firstSeen and updatedAt pair is what turns the baseline into change detection: a host with a recent firstSeen is new to the network.

Feeding results into rules

Probe results are available to the rules engine, so a scan finding can raise an alarm the same way a metric can — a new host appearing, a critical vulnerability, a port opening that should not be open. See Alarms and rules.

Results also flow to InfluxDB as metrics under the SECSCAN group with a probe tag, and are queryable through POST /auditlog with agent set to network, vulns, snmp or sniff.

Index