# Security Probes

## 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.

```mermaid
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.

{{< staticImage "edge_products/axiom_border/console-network-status.png" "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:

{{< staticImage "edge_products/axiom_border/console-host-ports.png" "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:

{{< staticImage "edge_products/axiom_border/console-host-vulnerabilities.png" "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.

{{% notice style="note" title="How to read a finding" icon="lightbulb" %}}
**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](ot_ics_scanning/).
{{% /notice %}}

## 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.

{{< tabs groupid="ui-or-api" >}}
{{% tab title="Web console" %}}
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.
{{% /tab %}}
{{% tab title="REST API" %}}

```bash
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}'
```

```json
{ "uuid": "4a79214d-3deb-4ae8-933f-9874cbdc1128", "status": "inprogress" }
```

{{% /tab %}}
{{< /tabs >}}

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`.

{{% notice style="note" title="Executions also publish over MQTT" icon="lightbulb" %}}
If you would rather not poll, the embedded broker publishes `started`, `finishedok` and `failed` events
for every execution. See [MQTT and OpenGate operations](../mqtt_and_opengate/).
{{% /notice %}}

## Network discovery and port scanning

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

```bash
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](../configuration/#securityprobesnmap).

## 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.

```bash
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](../configuration/#securityprobesvulnscan) 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](ot_ics_scanning/)** — Modbus/TCP, IEC 60870-5-104, DNP3, BACnet/IP
and OPC UA, with the two-lock model that governs write operations.

{{% notice style="warning" title="Check the templates directory before trusting an empty report" icon="triangle-exclamation" %}}
A web scan that reports nothing looks exactly like a web scan that had no templates to run. Confirm the
directory is populated:

```bash
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.
{{% /notice %}}

## 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](../mib_catalog/), which matters on an air-gapped probe where you cannot look an OID up.

```bash
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**:

```bash
# 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`.

{{% notice style="note" title="Prefer profiles over inline credentials" icon="lightbulb" %}}
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.
{{% /notice %}}

{{% notice style="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.
{{% /notice %}}

## 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.

```bash
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.

{{% notice style="warning" title="The interface name is the usual culprit" icon="triangle-exclamation" %}}
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`.
{{% /notice %}}

## 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:

```bash
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](../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

{{% children sort="weight" depth="100" %}}
