Skip to content

Detection models

Detection is deterministic statistics, running hourly on your own hardware against your own data. There is no cloud model and no shared baseline. What a Pro licence can add is a signed model pack — more YAML of exactly this language, loaded over the built-in set, never code and never a replacement for it. The optional language model explains incidents; it does not detect them. How the job that runs the detectors is scheduled and bounded — the minute timer, the hourly windows, the memory bound on every query and what its lag means — is on analysis runs.

Detector Asks
rarity Has this device ever done this before?
beaconing Is this contact suspiciously regular?
robust_baseline Is this hour unlike this device’s usual hours?
scan Is this device probing?
peer_isolation Is this device behaving unlike devices that behave like it?

First time this device reached this destination, this domain, this port, this TLS fingerprint, this user agent. Tracked per device and per network.

Rarity is the strongest single signal in a small network, because a home network is deeply repetitive: a camera talks to three places, forever. It is also the loudest, which is why it has the longest prior-observation requirement of any detector.

Regularity of the intervals between a device’s contacts with one destination. The measure is the coefficient of variation of those intervals: a human browsing is irregular, a scheduled check-in is not, and malware calling home is very much not.

Regularity alone is not enough, and since 0.38 the detector says so in its own measurement. Every beaconing result carries interval_cv, period_seconds, periodicity (how concentrated the spectrum is around the period), intervals (how many gaps were measured), span_seconds (how long the series ran) and destination, and the verdict is drawn from all of them: a real timer produces many gaps over hours, not six contacts in a burst. Two more steps follow the thresholds — a destination that many devices contact is a service, and a series that ran through the device’s learning phase is its job — and they are described under how a verdict is drawn.

Per device, per hour-of-week: bytes, flow count, destination count. The statistics are robust — median and median absolute deviation rather than mean and standard deviation — so one enormous backup does not raise the threshold for the next month. Deviation is reported as a z-score.

Unique destination ports contacted within a short window. This is the one detector that is closer to a rule than a baseline, and deliberately so: there is no normal amount of port scanning.

Since 0.38 the detector measures every destination group of a device before any group gets its verdict, and the result carries unique_ports, window_seconds, hosts (how many destinations of the same device reached the port count in the same evaluation — the fan-out) and destination. A scan is a sweep — the port count on at least minimum_hosts destinations — or a vertical scan of one host, which needs single_host_unique_ports. A busy application touching thirty ports on one server is neither.

Devices are clustered by behaviour — services, destinations, volumes, times — and a device that sits far from its own cluster is flagged. An isolation-forest score over hourly features per group.

This is the detector that partly replaces manual device typing: you do not have to tell Nyxtrace that these eight things are cameras, because they behave like each other.

A broadcast, a multicast group and a link-local address are not destinations a device chose, so beaconing and scan drop them before grouping, and the rarity destination features do the same. A television waking up and querying the SSDP group five times is neither a beacon nor a scan nor a first sighting. Feature kinds that are not addresses — domain, JA4, user agent — are untouched. See address classes.

A model is a detector plus parameters plus a base score. This is the set that ships in the binary’s model file — the built-in set, which every installation runs and which a model pack can extend but never switch off:

Model Detector Severity Base score Fires when
rare_destination_v1 rarity medium 45 A destination not seen in the prior 14 days
periodic_destination_v1 beaconing high 65 ≥ 12 intervals over ≥ 3600 s, coefficient of variation ≤ 0.10, fewer than 10 devices share the destination, not an expected series
hourly_volume_v1 robust_baseline medium 50 Hourly bytes, ≥ 8 samples, z ≥ 6
hourly_flow_count_v1 robust_baseline medium 50 Hourly flow count, ≥ 8 samples, z ≥ 6
hourly_destination_count_v1 robust_baseline medium 55 Hourly destination count, ≥ 8 samples, z ≥ 6
destination_scan_v1 scan high 70 ≥ 50 unique ports in 60 s on ≥ 2 destinations, or ≥ 200 on one
peer_isolation_v1 peer_isolation medium 60 Isolation score ≥ 70
rare_periodic_destination_v1 composite high 80 A new destination contacted periodically

The last one is the interesting one, and it shows what the model language is for. Neither half is alarming alone — new destinations happen daily, and plenty of legitimate things poll on a schedule. A new destination that is also being polled on a tight schedule is a different thing entirely, and it scores 80.

The two High models carry the thresholds of 0.38. Until then periodic_destination_v1 fired on five intervals at a coefficient of variation of 0.15 and destination_scan_v1 on twenty ports on any one host, and the forecast of the reference installation said what that would have meant on the day its devices matured: about three thousand High verdicts a day, nearly all of them infrastructure pollers, NAS and IoT doing their jobs on a timer. The rest of this page is what 0.38 added so that a true beacon is still found and a poller is not an incident — and every number is in the model file, none in the binary.

For the two destination-shaped detectors the verdict is drawn from the measurement in a fixed order, and the analyzer and the replay share the code:

  1. Thresholds. Beaconing: at least minimum_intervals gaps, interval_cv at or under maximum_coefficient_of_variation, a periodicity over the concentration floor, and a span of at least minimum_span_seconds. Scan: at least unique_ports on a destination inside window_seconds, on at least minimum_hosts destinations of the same evaluation or single_host_unique_ports on a lone one (0 = never).
  2. Role policy. A device whose role says suppress for the model has its verdict withdrawn; raise_threshold was applied before step 1 by laying the role’s parameters over the model’s.
  3. Shared destination (beaconing). A destination that maximum_shared_devices or more devices contacted in the last seven days is a service, not a beacon target — the DNS server, the NTP pool, the update mirror.
  4. Expected series (beaconing). The baseline below.

A verdict a later step withdraws is stored as not triggered with the measurement and a policy= token saying which step withdrew it — policy=suppress, policy=shared_destination,shared_devices=14 or policy=baseline,baseline_since=2026-09-12,baseline_days=6 — so the forecast counts what the tuned pipeline would fire, the device detail shows why a result did not, and the replay can redraw it under another file. The score is unchanged by any of this: (1 − cv) × 60 + periodicity × 40 for beaconing, unique_ports × 100 / threshold for scan.

A gateway touches many ports on many hosts by design; a monitoring host polls forty targets on a timer. Neither is an incident, and neither should need a false-positive verdict every morning. The model file’s roles section is the smallest scheme that says who and what:

version: 1
roles:
infrastructure:
match:
netbox_roles: [gateway, router, firewall, switch, access-point, hypervisor,
dns, dns-server, dhcp, dhcp-server, monitoring, backup,
nas, storage, printer]
device_types: [router, switch, firewall, gateway, hypervisor, nas, printer, server]
tags: [infrastructure, infra]
proxmox_guests: true
policies:
periodic_destination_v1: allow_known_destinations
destination_scan_v1: suppress
models:
- …

That is the role the file ships with. A role is a match and a set of policies:

Match key Satisfied by
netbox_roles the NetBox role of any of the device’s addresses
device_types the inventory device type — set by a connector, a classification or by hand
tags any inventory tag, or any Proxmox guest tag
proxmox_guests true matches every Proxmox VM and container the connector knows

Tokens are compared after normalisation — lower case, runs of anything that is not a letter or a digit become one dash — so a NetBox role “DNS Server” matches dns-server, and Infra, infra and INFRA are one tag. A device takes the first role in file order whose match it satisfies; a device that matches none keeps the default policy. UniFi reports network devices as identity anchors, not as device context, so a UniFi switch is matched by its inventory device type or a tag, not by a UniFi role.

Policy Effect
suppress the model is measured and stored for the device, never an incident
allow_known_destinations beaconing models only: an established series is expected whenever it began, not only inside the learning window — a poller’s new target fires while it is new and falls silent after baseline_minimum_days
{action: raise_threshold, parameters: {…}} the role’s parameters are laid over the model’s for this device; they validate like the model’s own ({action: raise_threshold, parameters: {z_threshold: 9}} on a baseline model, say)

The file is validated before use: every policy names a model of the file, allow_known_destinations is refused on anything but a beaconing model, and a raise_threshold with a parameter the detector does not know is refused. The runtime resolves a device’s role once per analysis pass, from the inventory and the connectors’ context; a tag takes effect at the next pass, a NetBox role or a Proxmox guest once its connector has polled. The detectors themselves know nothing about NetBox — they see a role name.

A device that polls the same destination at the same interval, day after day, is doing its job. Since 0.38 the analyzer keeps, in its engine snapshot, one record per device and destination that the beaconing detector has seen periodic — raw, before any policy: the first and last periodic observation, the number of distinct UTC days it was periodic on, and the period. A series is expected, and its verdict withdrawn as policy=baseline, when

  • it has been periodic on at least baseline_minimum_days distinct days (three), and
  • it began inside the device’s learning window — a series that first turned periodic after the device matured is new, fires, and keeps updating its one incident for as long as it lasts. A role with allow_known_destinations drops this second condition.

The record is written during the learning phase too; that is where it is learned, and the learning result of an established series reads learning baseline; …,policy=baseline,… with a negative verdict, so the forecast already reflects it. Days are counted monotonically — an out-of-order re-run of an older window extends the first sighting backwards but adds no day, so a replay can only make the table more conservative. A record silent for baseline_expiry_days (thirty) is forgotten; a period that moved by more than a factor of two under the same key starts a new record; a device is bounded at 64 records and, over the bound, loses its longest-silent ones and reports baseline_series_capacity on the pass.

What follows for a deployment. The baseline needs baseline_minimum_days of the 0.38 analyzer before a device matures. On the reference installation the devices of the 2026-09-10 cutover mature on 2026-09-24, so 0.38 has to be running by 2026-09-21 or the first days after maturity fire on series the analyzer has not had time to learn. The replay shows this honestly: its header says how many series it was seeded with.

Aggregation: one incident per pattern, a daily cap per device

Section titled “Aggregation: one incident per pattern, a daily cap per device”

An incident is keyed by tenant, model, device and pattern, so a series that keeps firing updates one incident rather than opening one an hour. 0.38 closes two gaps in that.

A sweep is one incident. The scan detector keys its incident by the destination’s /24 (IPv6 /64), so a device probing a subnet is one incident that grows — more hosts, more evidence, the highest score, the widest window — instead of one incident per host. Candidates of one fingerprint inside one analysis output are merged before anything is written.

A device opens at most five new incidents a day. aggregation.max_new_incidents_per_device_per_day (five; 0 switches the cap off) bounds what one device can open on one UTC day. A candidate whose incident does not exist yet, on a day the device has already opened the cap’s worth, is folded into the device’s overflow incident of that day: one incident per device and day, titled Further findings beyond the daily cap, whose summary reads “and N more on beyond the cap of 5 …” and lists the folded models with their counts, and whose models and evidence are the union of what was folded. It is an ordinary detection incident — listed, delivered to the sinks once, escalated only on a higher score — and the incident page says how to read it. An incident that already exists is updated whatever the count, and the folds are remembered per fingerprint so a re-run of the same window folds a candidate once. The pass reports incident_daily_cap with the number folded.

Everything above is a parameter in the model file, and the replay says what a change does on your data before it is live. The file the appliance uses is the bundle’s copy at /opt/nyxtrace/models/network-core.yaml, named by NDR_MODEL_PATH; an update replaces that copy, so a site-specific file lives elsewhere and NDR_MODEL_PATH points at it.

Knob Shipped Move it when
periodic_destination_v1.minimum_intervals 12 short bursts still fire → raise; a slow beacon (hours apart) is missed → lower, with a longer window
…maximum_coefficient_of_variation 0.10 jittered pollers fire → lower (0.05); a real beacon with jitter is missed → raise
…minimum_span_seconds 3600 five contacts a minute apart are five minutes, not a beacon
…maximum_shared_devices 10 internal services fire → lower; a wormed network would hide behind it → raise
…baseline_minimum_days / …baseline_expiry_days 3 / 30 the baseline
destination_scan_v1.unique_ports 50 twenty ports in a minute is a busy application; fifty is not
…minimum_hosts / …single_host_unique_ports 2 / 200 a sweep, or a vertical scan of one host
roles.infrastructure.match NetBox roles, device types, tags, Proxmox guests a poller still fires → tag it infra, or give it its NetBox role
aggregation.max_new_incidents_per_device_per_day 5 0 disables the cap

Why these numbers: on the release’s synthetic corpus — ten infrastructure pollers with four steady targets each, three monitors sweeping eight hosts on sixty ports, twenty cameras polling a vendor cloud, twenty workstations sharing a DNS server, five jittered pollers, one true beacon that starts on a mature workstation, one real sweep and a developer hitting sixty ports on one host — the previous file fired 545 High verdicts a day over 58 devices, the shipped file 4.0 a day over one, and what was left was exactly the true beacon and the real sweep. Each step did work: the thresholds on the bursts, the jitter and the developer; the roles on the monitors; the sharing step on the DNS server; the baseline on the pollers and the cameras. Your network is not that corpus, which is what the replay is for.

An older appliance must not be handed a 0.38 model file: it would ignore roles and aggregation as unknown keys, but it refuses the new parameters (minimum_span_seconds, minimum_hosts, …) and the analyzer would not start. Roll the bundle back with the file it brought.

Models are declarative YAML, validated before use. There is no code execution: a model is a combination of conditions over computed features, plus a score. The file has three top-level sections beside version: models, and since 0.38 the optional roles and aggregation.

version: 1
models:
- id: camera_reaches_unexpected_peer_v1
title: Camera reached a destination other than the recorder
severity: high
base_score: 75
conditions:
- feature: destination_rarity
operator: eq
threshold: 1
- feature: beacon_score
operator: gte
threshold: 40

Conditions are combined with AND. Features are the values the detectors compute; the operators are comparisons. Anything outside that vocabulary is rejected at load time, which is exactly what makes it safe to let a language model draft one — it produces a proposal that a validator checks and a person approves, and approving it does not activate it.

The scores decay over time per device, so a single event does not keep a device at the top of the list forever.

For the sake of not overpromising: Bayesian baselines, per-group autoencoders and sequence models over service order are planned, not present. They need four to eight weeks of data before they can be evaluated at all, so they follow the statistical layer rather than replacing it. See the roadmap.