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.
The five detectors
Section titled “The five detectors”| 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? |
rarity
Section titled “rarity”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.
beaconing
Section titled “beaconing”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.
robust_baseline
Section titled “robust_baseline”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.
peer_isolation
Section titled “peer_isolation”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.
What the detectors ignore
Section titled “What the detectors ignore”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.
The shipped model set
Section titled “The shipped model set”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.
How a verdict is drawn
Section titled “How a verdict is drawn”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:
- Thresholds. Beaconing: at least
minimum_intervalsgaps,interval_cvat or undermaximum_coefficient_of_variation, a periodicity over the concentration floor, and a span of at leastminimum_span_seconds. Scan: at leastunique_portson a destination insidewindow_seconds, on at leastminimum_hostsdestinations of the same evaluation orsingle_host_unique_portson a lone one (0 = never). - Role policy. A device whose role says
suppressfor the model has its verdict withdrawn;raise_thresholdwas applied before step 1 by laying the role’s parameters over the model’s. - Shared destination (beaconing). A destination that
maximum_shared_devicesor more devices contacted in the last seven days is a service, not a beacon target — the DNS server, the NTP pool, the update mirror. - 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: 1roles: 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: suppressmodels: - …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.
Expected periodic destinations
Section titled “Expected periodic destinations”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_daysdistinct 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_destinationsdrops 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 incident_daily_cap with the
number folded.
Tuning the thresholds
Section titled “Tuning the thresholds”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.
Writing your own
Section titled “Writing your own”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: 1models: - 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: 40Conditions 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.
What is not in the product
Section titled “What is not in the product”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.