Skip to content

Changelog

Release notes for the appliance and its interface, written from the user’s point of view. R-numbers are interface and appliance build labels; features behind a feature flag are marked as such.

0.39 — The server-side Pro value exists: a signed threat-intel feed, signed model packs and the analyst quota (R46)

Section titled “0.39 — The server-side Pro value exists: a signed threat-intel feed, signed model packs and the analyst quota (R46)”

This release is being prepared; the notes describe what it contains. The licensing page has said since the first release that part of what Pro sells lives on the server rather than in the binary. Until now that part was a stub. 0.39 makes it real — three things behind the entitlement token, none of which a patched client can give itself, and none of which can stop the free core. The other half is the Nyxtrace licence server 0.4.0, which serves the two content channels and carries the quota, and which has to be in place before an appliance on 0.39 sends its first heartbeat; see updating to 0.39. The catalogue of what a Pro installation receives, what a free one gets instead and how far the download gate honestly reaches is a new page, Pro content.

  • A curated, signed threat-intel feed (licence feature feeds). One bundle a day from the key server — abuse.ch Feodo Tracker, SSLBL and URLhaus, normalised — signed with a feed key of its own (nyx-feed-2026a, a third keyring at /etc/ndr/feed-pubkeys.json), valid 14 days, verified, checksummed, parsed with the API’s own strict parser and counted against the signed number before it becomes a candidate. The refresh unit now runs as the service user and installs the candidate in a root step that treats it as hostile input — no symlink, right owner, no group or other write, O_NOFOLLOW and an inode comparison, re-validated, never empty. Not entitled, or any failure of the pull, falls back to the local sources: a Pro installation is never worse off than a free one, and the reason is on the health row. ndr threat-intel pull|status. See threat intelligence.
  • Spamhaus DROP left the default local source set — it is free for non-commercial use only. The three abuse.ch sources (CC0) stay; a locally built file now has an empty cidrs list, and the local fetch is bounded at 32 MiB per source and 96 MiB in total. An empty upstream answer keeps the running file instead of switching detection off.
  • Signed model packs (licence feature model_packs). ndr models pull|status|validate and a daily ndr-model-packs.timer: a pack is signed with the release key, valid 180 days, refused when it needs a newer appliance, and every model in it goes through the analyzer’s validator before it is installed under /var/lib/ndr/model-packs/<id>/<version> with a current.json pointer. Packs load over the built-in model — a pack model replaces a built-in id in place, packs apply in id order, the first wins a collision, a pack that stops loading contributes nothing and never stops the built-in model. With the 0.38 roles: a pack may add roles for its own models, the shipped roles come first and win, aggregation stays the built-in file’s, and a pack that would leave a shipped policy invalid is dropped whole. The catch-up loop reloads a pull before its next pass. New health row models; rollback is a current.json edit. See model packs.
  • The analyst quota (licence feature analyst). The entitlement token carries analyst_quota_microusd and analyst_quota_period (month): a budget per installation and UTC month — 5, 10 or 20 USD by plan, per key adjustable in the portal — asked immediately before the daily budget is reserved, across every tenant. A run beyond it is 429 quota_exhausted, distinct from budget_exhausted, your own daily cap, which applies unchanged. Best effort, not a proxy: the OpenAI key and the ledger stay yours, the appliance refuses by itself, the key server shows the reported usage and flags an installation over quota and enforces nothing. New health row analyst; GET /api/v1/license carries the two fields. See the licence quota.
  • The heartbeat may carry an analyst object — period, spent_microusd, runs, three bounded numbers, Pro with a quota only. A key server older than 0.4.0 refuses the whole heartbeat with a 400; the appliance then retries at once without the object, then without analyst and update, and holds the dropped object for a day — the 0.26 rule, one step further down the ladder. ndr telemetry show prints an analyst report: line. See the analyst usage report.
  • The honest limit, stated. The bundle bytes live in a content bucket that must deny listing; the object’s name is the signed manifest’s bundle field, which must carry 16 random hex characters and is handed only to a holder of an entitled token. The gate is exactly “you cannot guess the name”; a presigned, licence-bound URL is the stated upgrade. What every installation gets regardless of the bucket: a bundle is only ever accepted with a valid signature from the right key for its type, and nothing downloaded is executed. See Pro content.
  • Health and API. The threat-intel row carries source, bundle_version, signing_key_id, pulled_at and the admin-only pull_error; models and analyst are new rows that never change the overall status; quota_exhausted joins the stable error codes; four environment keys with defaults (NDR_FEED_KEYRING, NDR_THREAT_INTEL_STATE_DIR, NDR_MODEL_PACK_DIR, NDR_CONTENT_CHANNEL). No ClickHouse migration, no new grant.
  • Licence server 0.4.0 (the other half, released 2026-09-19): the two content channels with publish, withdraw and republish and their portal pages, 402 not_entitled for a valid token without the feature, the feed index and pack index under /v1/entitled/… no longer stubs, the analyst quota on plans and keys with a portal dialog, and usage against quota on the instance and key views. Its review before the merge closed a paywall bypass through a guessable object name, an unbounded upstream fetch and an unbounded index — all before anything was published.

0.38 — The detectors are tuned before the learning phase ends, and the dashboard speaks English in its URLs too (R45, R47, 2026-09-19)

Section titled “0.38 — The detectors are tuned before the learning phase ends, and the dashboard speaks English in its URLs too (R45, R47, 2026-09-19)”

This release is being prepared; the notes describe what it contains. Two things in it. The detection tuning answers the forecast of 2026-09-18 on the reference installation — about three thousand High verdicts a day waiting for the day its devices mature, nearly all of them pollers doing their jobs — with a replay that says what a model file would fire on your own data, a learned baseline of expected periodic destinations, role policies for infrastructure and a daily incident cap per device. It has to be running three days before a device population matures (by 2026-09-21 on the reference installation), or the baseline has nothing learned on the day it is needed; see before the learning phase ends. It carries no ClickHouse migration; ndr migrate adds one SQLite table.

The second thing: the interface had been English since R14; its identifiers — the paths in the address bar, the query parameters, the names of the demo devices — were still German. That is over. Old links keep working: every old path redirects permanently and old query parameters are rewritten once in the browser, so a bookmark or a link in a chat message lands on the English form from then on. Nothing in the API or the BFF allowlist changed for that part. See the URLs changed in 0.38.

  • A replay harness. ndr analysis replay [--tenant] [--days 1..7] [--model FILE] [--json] re-evaluates the stored results of the last days against the configured model file and a candidate — thresholds, roles, shared destinations, the baseline — and prints per model what was evaluated, what was stored (the forecast’s number), what would fire, over how many devices and series, what each step dropped and the top devices; in total the severities and the incidents a day before and after the cap. Read-only: no result, no incident, no lease. ndr analysis forecast --model FILE and GET /api/v1/analysis/learning-forecast?model=<file> join the forecast with a candidate’s titles and severities (model_file in the response); the counts stay the stored verdicts. See replaying a model file.

  • The reason is a measurement. A beaconing result carries interval_cv, period_seconds, periodicity, intervals, span_seconds and destination; a scan result unique_ports, window_seconds, hosts and destination; a step that withdraws the verdict appends policy=suppress, policy=shared_destination, shared_devices=N or policy=baseline,baseline_since=…,baseline_days=N. No schema change. Results written before 0.38 lack the new tokens and the replay counts them as legacy.

  • Expected periodic destinations. The analyzer keeps, per device and destination, when a series was first and last periodic, on how many distinct days and at what period. A series periodic on three distinct days that began inside the device’s learning window is expected and silent; a series that first turns periodic after the device matured is new and opens one incident that updates itself. Records expire after thirty silent days; a period that moves by more than a factor of two is a new series; 64 records per device. See expected periodic destinations.

  • Roles. The model file has a roles section: a role matches on NetBox roles, inventory device types, inventory or Proxmox tags, or every Proxmox guest, and sets a policy per model — suppress, allow_known_destinations (beaconing only: an established series is expected whenever it began) or raise_threshold with parameters. The shipped infrastructure role suppresses scans and allows known destinations for gateways, hypervisors, DNS and DHCP servers, monitoring, backup, NAS, printers and every Proxmox guest. UniFi network devices are matched by device type or tag. See roles.

  • Stricter candidates. periodic_destination_v1 needs 12 intervals (5) over at least 3600 s at a coefficient of variation of 0.10 (0.15), and a destination ten or more devices contacted in seven days is a service, not a beacon target. destination_scan_v1 needs 50 ports (20) inside a minute on at least two destinations of the same evaluation, or 200 on one. On the release’s synthetic corpus of 61 devices the previous file fired 545 High verdicts a day and the shipped file 4.0, leaving exactly the true beacon and the real sweep. See tuning the thresholds.

  • Aggregation. A scan incident is keyed by the destination’s /24 (IPv6 /64), so a sweep is one growing incident; candidates of one fingerprint in one output are merged. A device opens at most five new incidents a day (aggregation.max_new_incidents_per_device_per_day, 0 = off); further candidates fold into one overflow incident per device and day, Further findings beyond the daily cap, whose summary reads “and N more on …” and which merges the folded models and evidence. An ordinary detection incident on the wire and in the interface. See the incident page.

  • Rollback. A 0.38 model file must not be handed to an older binary: it ignores roles and aggregation but refuses the new parameters and the analyzer does not start. The previous bundle brings its own file.

  • Routes. /anmelden → /login, /lage → /overview, /geraete and /geraete/<id> → /devices and /devices/<id>, /ziele → /destinations, /suche → /search, /vorschlaege and /vorschlaege/<id> → /proposals and /proposals/<id>, /mandant → /tenant. Each old path answers with a permanent redirect (308) to its successor, query string included.

  • Query parameters and their values. Shared time range and scene: modus → mode, fenster → window, von → from, bis → to, tempo → speed, fokus → focus, fokusart → focus_kind (knoten → node), szene → scene (tabelle → table). Network map and device workspace: ansicht → view (netz → network), rueckkehr → return. Proposals: art → kind, ziel → target. Destinations: land → country, protokoll → proto, sortierung → sort. Devices: typ → type, risiko → risk, seit → since. Incidents: schwere → severity, zeitraum → in_window. Search: sm → search_mode. Sign-in: weiter → next, fehler → error (anmeldung → sign_in, keine_berechtigung → no_authorization, mehrere_mandanten → multiple_tenants; idp and tenant were already English), abgemeldet → signed_out. asn, vlan and q were already English and are unchanged.

  • One-way compatibility. A request that still carries an old key or an old value is read exactly like the new form and the visible URL is rewritten once; the two server-rendered pages (/login, /tenant) redirect to the canonical URL instead. There is no plan to read the old names forever.

  • Demo names. The synthetic demo data speaks English as well: demo-heim → demo-home, demo-kamera-0N → demo-camera-0N, demo-drucker-01 → demo-printer-01, demo-gast-0N → demo-guest-0N, demo-lampe-0N → demo-light-0N; the demo personas leser/leser-zwei are reader/reader-two.

0.37 — The response chain can be proven without a real incident, response targets report their health, and a rollback gives every file back to its owner (R43, R44)

Section titled “0.37 — The response chain can be proven without a real incident, response targets report their health, and a rollback gives every file back to its owner (R43, R44)”

This release is being prepared; the notes describe what it contains. It answers the findings of the first live response activation and of the first update that went through the updater end to end. It carries no ClickHouse migration and no unit change; the update restarts ndr-api and nothing else. See updating to 0.36 and 0.37.

  • A synthetic canary finishes the activation. POST /api/v1/response/canary (admin) creates an incident of kind canary with a quarantine_device proposal for an address you own, gated exactly like a detection; from there the chain is the real one — approve, standby-first, Release, expiry — so the dry run under kill_switch: true, the held entry on both nodes and the fail-open with the service stopped can all be seen while the appliance has no real incident. A canary is never counted in the calibration, never a candidate of the automatic mode, and reaches a sink only as response.canary, never as an incident or a real response event; the Response card says Canary · synthetic chain check, not a detection and the worker cleans it up at its ttl. Incidents carry kind, actions canary, both additive. See the canary and part 3 of the OPNsense page.
  • Response targets are probed every ten minutes. An action connector never polls, so its health line stayed unavailable although test --live passed on both nodes. The service now runs the companion’s health on both nodes (the controller session for UniFi) at start and every ten minutes and the line follows it: ok, degraded with the companion’s own findings, unavailable with the failure class, stale after three missed probes. See the target’s health line.
  • A quiet start. The worker’s reconciliation no longer logs invalid node attempt on an appliance with empty response tables: a listing without an action is not persisted, and without a current membership no node is contacted.
  • The updater’s install step needs no NDR_ARTIFACT_DIR. The bundled installer resolved its first-install artefact directory before it looked at --from-bundle, which ended every real updater install in stage install; the directory is now resolved only on a first install, and ndr update starts the installer with the variable removed. See the install step and the rollback.
  • A rollback restores owners, and verifies them. The snapshot carries a manifest of owner, group, mode and mtime of every file; the restore re-applies and checks it, and a difference ends the attempt rolled_back_unhealthy naming the path instead of leaving a sites.yaml the service cannot read. A snapshot from an older updater has no manifest and --rollback refuses it. The first update onto 0.37 still snapshots with the old updater — the one-time trap.

0.36 — The updater’s health gate reads a loopback route, and the first signed update went end to end (R42, 2026-09-19)

Section titled “0.36 — The updater’s health gate reads a loopback route, and the first signed update went end to end (R42, 2026-09-19)”

Deployed to the reference installation the same day, and the first release whose successor arrived through the updater: 0.36.0 → 0.36.1, a stamp-only patch, was published to beta, verified, downloaded, snapshotted, installed, restarted and passed the health gate — applied/done in about seventy seconds — after a first attempt had surfaced the two defects 0.37 fixes. See the health gate.

  • GET /healthz, internal. A loopback-only, unauthenticated view of the appliance’s own health — the overall status, the version and every component as id, kind and status, nothing else — refused for any non-loopback peer or any proxy header (403) and rate limited. Made for the updater; useful for a curl on the box. See the endpoints.
  • The health gate reads it by default. Until now the gate’s default was the signed /api/v1/health, which the root updater cannot sign — the first real ndr update --apply --force stopped in the preflight with 401, fail-closed and nothing mutated. NDR_UPDATE_HEALTH_URL now defaults to http://127.0.0.1:8080/healthz; an operator who points it at the signed route gets the probe signed with the service secret. A refused probe prints health gate: source=… url=… http=… and a hint, never the body.
  • A failed attempt no longer degrades update for good. It counts only until a later check succeeds — then 24 hours of stale, then ok — while rolled_back_unhealthy never fades. The component carries last_attempt_at, last_attempt_outcome, last_attempt_stage and the admin-only last_attempt_error. See the update component.

0.35 — The analyzer keeps up: a time-keyed flow copy with a pair projection, a catch-up loop paced by the load, and the throughput on the health report (R41, 2026-09-19)

Section titled “0.35 — The analyzer keeps up: a time-keyed flow copy with a pair projection, a catch-up loop paced by the load, and the throughput on the health report (R41, 2026-09-19)”

Deployed to the reference installation on 2026-09-18 — by hand from the verified bundle, because the updater of the running release stopped in its preflight (the finding 0.36 answers). 0.34 no longer ran out of memory, but it drained about 0.055 hours of backlog per minute of running — every flow query was a full scan of the flow table’s retention — and only when it ran back to back, which drove the load to 21 on sixteen cores. The update restarts ndr-api and the analyzer timer, in that order, and nothing else; the migration’s backfill takes one to three minutes of the API’s start. It is also the first release published to the release channel for the signed updater. See updating to 0.35.

  • The flow phases read a time-keyed copy with a pair projection. The canonical flow table is keyed by flow id — right for the ingest, no key for a time range — so the hour aggregate of one window read 24 million rows and 3.4 GiB, and the changed-flows page, at 77 % of the analyzer’s database time, read the whole retention twice per call. Migration 013 adds canonical_flows_hourly: the analyzer’s fifteen columns ordered by tenant, hour and flow id, a projection by_pair in pair order, a materialised view from the source and a one-time backfill (one to three minutes on the reference installation, no new grant, about two fifths of the source table’s disk). The page is read a batch of eight devices at a time, its changed-pair subquery is bounded to the slice’s own versions instead of everything since the checkpoint, and no analysis query uses FINAL any more. On the integration corpus one page call reads 102,400 rows instead of 1,536,000 (15×) and the hour aggregate 18,432 instead of 1,011,712 (55×), with identical results. See analysis runs and disk sizing.
  • A catch-up loop, paced by the load. ndr-analyze-loop@<tenant>.service (ndr analyze --loop) runs cycles of the analysis back to back while the job is behind, pauses when the one-minute load average is above NDR_ANALYZE_LOAD_CEILING (three quarters of the CPUs by default), sleeps a minute between cycles once live, commits the pass in flight on a stop signal, and stops itself when its binary is replaced so systemd starts the new one. The installer still enables the timer; the loop is switched on by hand for a backlog and off again once lag_seconds is under an hour — exactly one of the two per tenant. See catching up: the loop.
  • The timer fires every two minutes with a 110 s wall. The minute slot with a 50 s wall drove the load to 21 on sixteen cores when the job was behind; the two-minute slot with a 110 s wall was measured at a load of at most 12 with the API under two seconds, and is now the default of NDR_ANALYZE_MAX_WALL and the shipped unit. A hand-set 110s or a timer drop-in can go. Every analysis query also runs niced (os_thread_priority 10, NDR_ANALYZE_QUERY_NICE) so it yields to the API and the ingest.
  • Reading the throughput. The health report’s analysis object carries mode (timer or loop), cycles, paused_for_load, load, load_ceiling, backlog_rate_hours_per_hour — hours of windows drained per hour of wall clock, 1.0 keeps up exactly — and eta_seconds, the time until live at that rate. On /metrics: nyxtrace_analysis_mode{mode}, …_cycles, …_paused_for_load, …_load_ceiling, …_backlog_rate_hours_per_hour, …_eta_seconds. The loop’s heartbeat is one row of analysis_service in the state database.
  • The release channel is live. The Nyxtrace server serves signed release manifests on stable and beta, and the release keyring — one key, nyx-release-2026a — is published at https://nyxtrace.com/release-pubkeys.json. Releases go to beta first with a rollout of 0 % and are promoted to stable once they have held; under the default policy an appliance still installs nothing on its own. See the release channel.

0.34 — The analyzer catches up: an hour-keyed rarity aggregate and a slice that remembers what fits (R40, 2026-09-18)

Section titled “0.34 — The analyzer catches up: an hour-keyed rarity aggregate and a slice that remembers what fits (R40, 2026-09-18)”

Deployed to the reference installation on 2026-09-18. It follows 0.33 on the same day, because 0.33’s analyzer did not catch up on the reference installation: nine runs, none drained, the checkpoint 75 hours behind. The update restarts ndr-api and the analyzer timer, in that order, and nothing else — no ClickHouse restart, no transport restart; see updating to 0.34.

  • The rarity phase reads an hour-keyed aggregate, not the raw tables. The query 0.33 kept failing on — the feature keys observed inside the window — had a predicate on the receipt time, and nothing in flow_observations or raw_events is keyed or indexed by it, so ClickHouse read every part of both tables, FINAL, five times over: 65 million rows and 25 GiB for a one-hour window of 76,000 keys, refused at the 4 GiB bound seven times out of seven. Halving the window never helped, because the cost did not depend on the window. Migration 012 adds feature_hourly — one narrow row per tenant, feature kind, value, device and hour, fed by two materialised views, 90 days for every kind — and the three rarity queries read only that: the window’s partitions for the candidates, primary-key reads in chunks of 1,000 keys for the history. On the integration corpus the candidate query reads 3 of 22 parts at a 6 MiB peak. The migration backfills the table in the same file (about a minute on the reference installation) and needs no new grant. See analysis runs.
  • The slice is state, and the search for one that fits survives the run. The width the analyzer works at is remembered in the state database (analysis_slice); a run starts there. A pass ClickHouse refuses for memory or cuts at the query timeout halves the window, persists the halved width first, and retries with a wall budget of its own — 0.33’s refused query had eaten the budget and the retry never happened. At most three halvings per run and never below a five-minute floor (was fifteen); either way the run fails with the reason, the analysis component goes degraded, and the next run continues at the width reached. The width grows back after six drained windows in a row, and never into a width refused less than six hours ago. A new window is also sized by its row count before it is opened (NDR_ANALYZE_SLICE_ROWS, 100,000 flows). See the slice is state.
  • Faster detection of a query that does not fit. max_execution_time is 30 s per analysis query (NDR_ANALYZE_QUERY_TIMEOUT, was 120 s), and a query cut by it halves the window like a memory limit; max_threads is 4 (NDR_ANALYZE_QUERY_THREADS), which alone divides the peak of an aggregation by four against the stock sixteen. Both are new keys in ndr.env, optional.
  • Reading it. On the health report analysis.slice_seconds is now the width the job works at, with slice_configured_seconds, slice_limit_at/slice_limit_class/slice_limit_seconds, last_error_class and last_run_slice_seconds beside it; last_error reads memory_limit at slice 1h0m0s: … or timeout at slice …: …. On /metrics: nyxtrace_analysis_slice_configured_seconds, …_slice_limit_seconds, …_slice_limit_timestamp_seconds{class}, …_last_run_slice_seconds, …_last_run_bound{class}. The run log carries slice_seconds and error_class.
  • The timer cadence. The shipped unit still fires every minute. An appliance whose timer was set back to hourly during the 0.33 incident keeps that until the analyzer is live — live=true, lag_seconds under an hour — and then returns to the minute cadence. Superseded in 0.35: the timer fires every two minutes with a 110 s wall, and a backlog is worked off by the catch-up loop.

0.33 — Operations findings of the second production week: sliced analyzer under a memory bound, receipt retention, Suricata stats drop, both-directions mirror, bounded ClickHouse logs, threat-intel hot reload, learning-phase forecast (R37, R38, R39, 2026-09-18)

Section titled “0.33 — Operations findings of the second production week: sliced analyzer under a memory bound, receipt retention, Suricata stats drop, both-directions mirror, bounded ClickHouse logs, threat-intel hot reload, learning-phase forecast (R37, R38, R39, 2026-09-18)”

Deployed to the reference installation on 2026-09-18. Four restarts apply it, in a fixed order — ndr-api, then ClickHouse once, then the transport, then the analyzer timer; see updating to 0.33.

  • The analyzer catches up instead of falling behind, and no longer runs out of memory. The hourly job had a fixed budget below the reference installation’s inflow, so its backlog was structural, and after five days its rarity query — which sorted the tenant’s whole retained history in memory — exceeded the database’s limit on every run. Now ndr analyze runs every minute but opens a new window only once its checkpoint is an hour old, so the cadence is unchanged; a job that is behind uses up to NDR_ANALYZE_MAX_WALL (50 s) of every minute, works the backlog off in windows of at most NDR_ANALYZE_SLICE (1 h), and halves a window ClickHouse refuses for memory down to a 15-minute floor. Every analysis query is bound to NDR_ANALYZE_QUERY_MEMORY_BYTES (4 GiB), spills to disk at half, and is killed after 120 s; the rarity history is two bounded steps over the keys in the window. Superseded in 0.34: the floor is five minutes, the timeout 30 s, the halved width is persisted and retried under its own budget, and the rarity phase reads feature_hourly — 0.33 alone did not catch up. The health report gains an analysis component and object (lag_seconds, the open window, the last pass, last_error) — stale and the appliance degraded beyond three hours behind — plus nyxtrace_analysis_* on /metrics and a seven-day run log in SQLite. See analysis runs.

  • The ingest receipt ledger is bounded to seven days. event_receipts, the replay check, grew by six million rows a day toward a 52–57 GiB steady state under a 31-day retention pruned by one inline DELETE. Retention is now NDR_RECEIPT_RETENTION (168h, 24 h to 90 d) — it only has to outlast the longest upstream replay, a firewall’s 64-hour syslog spool — and ndr jobs sweeps it every minute in batches of 5,000 rows, at most 20 s a tick. The sweep’s measurement is ingest.receipts on the health report and nyxtrace_receipts_* plus nyxtrace_state_db_bytes on /metrics. The file does not shrink when rows go — SQLite reuses the pages, and there is deliberately no VACUUM. See disk sizing.

  • Suricata stats records are dropped and counted, not quarantined. They were 888 of 894 quarantine rows on the reference appliance, 13–16 MB a day, every one failing the same way because the envelope had no mapping for the type. A stage in front of each firewall envelope now drops them deliberately, and because this build of the transport cannot export its internal counters, each one leaves a short line in /var/lib/vector/dropped/suricata-stats-<day>.ndjson (30 days). The envelope itself is null-safe and every quarantine decision is an explicit reason in metadata.dropped.message — an unknown event type shows up as unknown suricata event_type: <name>, one readable line per record. The quarantine directory still has no retention of its own. See firewall feeds.

  • The Proxmox mirror copies both directions. ndr-mirror now mirrors each guest tap on its ingress and its egress hook (NDR_MIRROR_DIRECTIONS=both, the default), so every session a mirrored guest takes part in is seen in both directions whatever its peer is. The reason was a scoped pilot that mirrored two taps without the uplink and saw DNS queries without a single answer — no dns.log, every TCP session half-seen. The uplink is no longer a source in this mode; NDR_MIRROR_DIRECTIONS=ingress restores the previous behaviour exactly and is the rollback switch. An existing install gains the egress filters on its next reconciliation — sync on a timer install, the same add per source on a hand-scoped one. Two mirrored guests on the same host that talk to each other now arrive at the sensor twice, once per port, like a SPAN of two switch ports. A legacy ingress qdisc with foreign filters stops preflight in both mode, because egress needs clsact. See Proxmox vSensor.

  • ClickHouse’s own logs are bounded. The database had been keeping every Trace-level line of every query without a TTL: 69.7 GB in eight days on the reference appliance, three times the product’s data. A shipped config.d fragment limits text_log to warnings and three days, the trace, part, processor, query-thread and metric logs to three days, and query_log to seven. It is installed by the fresh install and by the update, but ClickHouse reads it only at start — restart clickhouse-server once after the update and drop any rotated _N system tables. See disk sizing.

  • The threat-intelligence refresh restarts nothing. ndr-api follows the indicator file: it checks modification time and size at most once a minute and swaps the set atomically when they change. A rejected or unreadable newer file keeps the previous set matching and turns the threat-intel health line degraded, which now also carries loaded_at and entries. The daily try-restart of the API is gone; the API and the response worker share one set. The update path now also installs the refresh units it had previously placed only on a fresh install. See threat intelligence.

  • The learning phase has a forecast. Detectors now compute the real score and verdict for a device inside its learning window and store the result suppressed, with the reason learning baseline; <measurement> — it still reaches no incident, sink, metric or risk, and the device’s score history leaves it out. GET /api/v1/analysis/learning-forecast and ndr analysis forecast report, per model, how many suppressed results would have fired and on which devices. Migration 011 adds the triggered column without a new grant; start ndr-api before the analyzer’s next run. Results stored before this release carry score 0 and no verdict and cannot be backfilled, so the first useful reading is a day after the update. See the learning phase.

0.32 — The automatic mode: producer, identity proof, evidence classes, feedback and the calibration gate (R35, R36, 2026-09-12)

Section titled “0.32 — The automatic mode: producer, identity proof, evidence classes, feedback and the calibration gate (R35, R36, 2026-09-12)”

The automatic mode has a producer. It is off by default twice over: it runs only when the response target says auto_enabled: true and the tenant’s calibration gate is open, and the default of both is no — an appliance that touches neither behaves exactly as in 0.31. Nothing was deployed with either switched on.

  • A producer for the automatic mode. Every 30 seconds, per tenant with auto_enabled: true, the open incidents of the last 24 hours at or above automatic_score (95) and automatic_min_severity (high) are candidates. One that clears every gate — one device with one address, inside the allowed and outside the protected scopes, an identity proof of dhcp_lease_and_mac or better, at least two independent evidence classes at 90 % confidence, nobody acting against the incident, room under the cap and out of the cooldown, and the calibration gate open — is created and approved by nyxtrace:automatic and executed standby-first like any approval. One that clears the ladder but not a gate becomes a proposal for a person with the reason on it. One decision per incident, kept by the database against a second worker. See how a candidate becomes automatic.
  • An identity proof with four grades. enrolled, static_binding, dhcp_lease_and_mac, none — graded from the anchors observed on the target address in the last 24 hours (lease and controller MACs, inventory assignments, 802.1X/agent anchors, resolver clusters). A randomised MAC, a stale lease, a MAC only the mirror saw, or two identities on one address is none. Connector-written anchors count. Without a dhcp-leases or unifi connector everything stays a proposal — by design.
  • Evidence classes with an independence rule. Seven classes — Zeek, Suricata, NetFlow, DNS, mail, threat intel, behaviour model; two records of one family are one class, and a threat-intel match replaces the family of the record it was seen on, so one record is never two classes. Eight markers explain without counting.
  • Confidence is the evidence’s, not the score’s. A noisy-OR over the independent families and markers (threat intel 0.65, Suricata/Zeek 0.50, model 0.45, mail 0.40, NetFlow/DNS 0.35; resolver block 0.35, beaconing/scan 0.30, …), scaled by the resolved share. A model plus one mirror family reaches 0.73 and never becomes automatic; three agreeing families with a marker pass 0.90. Versioned as confidence_revision.
  • The calibration gate and the learning phase. Nothing runs automatically before 2026-09-24 (learning_until; the word none switches the phase off). Afterwards the gate opens only with at least five incidents carrying a true/false-positive verdict in the last 14 days at 80 % precision — an incident is one label, by its latest verdict, however often it was labelled. Persisted in the state database; read with ndr response calibration, GET /api/v1/response/calibration, or the new calibration line at the top of the Response page.
  • Response feedback. POST /api/v1/incidents/{id}/response-feedback with true_positive, false_positive or unknown — responder role, not analyst, because a false_positive releases every held quarantine of the incident and cancels a pending automatic action. See response feedback.
  • Caps that reopen instead of dropping. The automatic cap counts memberships held plus automatic actions approved but not yet applied; an action the executor still refuses for the cap or the cooldown is reopened as a proposal for a person (policy_decision.reopened_from). Every other gate is re-checked at apply time and ends the action failed with the reason, never downgraded.
  • The Why block on the response card (R36): identity grade with anchor count and freshness, evidence classes and markers in plain words, and the policy decision with its reasons — for every action, human or automatic. The API carries the same as identity_proof and evidence.
  • Four response events on the webhook and chat sinks. response.proposed, response.executed, response.failed, response.released, with the incident snapshot and a response object that carries the target address and the identity grade — never an alias name, a node address or a state id. An instance with an explicit events list does not get them until you add them.
  • The response target’s policy grew automatic_min_severity, learning_until, calibration_window_days, calibration_min_labels and calibration_min_precision_percent; a human approval now stores policy_decision.mode: "proposal" rather than its eligibility.

0.31 — Automatic response: OPNsense / pfSense quarantine and UniFi client block (R34, 2026-09-12)

Section titled “0.31 — Automatic response: OPNsense / pfSense quarantine and UniFi client block (R34, 2026-09-12)”

An approved quarantine can now be carried out. Pro, licence feature response, and off by default: without a response target in connectors.yaml the Response page records proposals and decisions and executes nothing, exactly as before.

  • A firewall quarantine through a fixed-scope companion. id: opnsense-response puts the target address into the tenant’s quarantine alias on an OPNsense or pfSense HA pair — standby node first, then the active node, with a readback on each, and executed only when both hold the entry — and optionally terminates the target’s routed states. There is no firewall API key: a small static binary, nyx-companion, runs on each node as an SSH forced command for a dedicated account, accepts one signed request, knows six operations and six exact pfctl argument forms, keeps the alias names and a second copy of the protected scopes in its own root-owned configuration, checks its own sudo grant, never flushes an alias and never deletes an entry it did not make. See OPNsense / pfSense quarantine.
  • A UniFi client block. id: unifi-response blocks the wireless client that holds the address on a UniFi controller and unblocks it on release, with a durable ownership ledger so a restart never lifts a block a person made. No fail-open on the controller — the page says so. See UniFi client block.
  • TTL with fail-open. Every membership has a deadline (ttl, default 15 minutes); the appliance releases at the deadline, and on the firewall the alias expiry cron releases regardless of whether the appliance is alive. The interface says so next to every lease.
  • Release. A Release button on the Response page and POST /api/v1/response-actions/{id}/release; an action is rolled_back only after every node confirmed absence. Release is never licence-gated.
  • Protected scopes, three layers. What you list, plus the node and controller addresses and the appliance’s own addresses implicitly, plus the firewall’s interface addresses on the companion’s side; allowed_targets refuses a prefix broader than /16 or /48 without allow_broad_targets.
  • Five new statuses and three additive fields. executing, reconciling, rollback_requested, rolling_back, expired; execution, lease_expires_at, policy_decision on every action, and GET /api/v1/response-actions/{id} with the per-node attempts. Only a failure class is stored — no error text, no node address.
  • Every human approval runs in proposal mode. The automatic mode has its policy fields (auto_enabled and the thresholds) but no producer yet; identity proof is none and the gate’s automatic path cannot pass — superseded in 0.32, which built the producer and the proof. The spec’s proofs on an isolated firewall pair and the live canary are still open — read the overview before the first approval.

0.30 — Chat notifications: Telegram, Discord, Slack, Matrix (R33, 2026-09-12)

Section titled “0.30 — Chat notifications: Telegram, Discord, Slack, Matrix (R33, 2026-09-12)”

The events 0.29 gave the webhook, as a message a person reads. All four are Pro, granted by the same connectors feature as the connector pack.

  • Four chat sinks. id: telegram, discord, slack and matrix in connectors.yaml post every new incident, every score that rose, every closure and every health component that changed state as one short message: the severity and title, score and status, the device names, the first line of the summary and a link into the dashboard. Telegram is a bot from @BotFather and a chat id (HTML, previews off, topics and silent delivery); Discord is a channel webhook and one coloured embed per event with mentions switched off; Slack is an incoming webhook or chat:write through the Web API, as Block Kit; Matrix is a bot account, an access token and a room id (no aliases), with the event id as the transaction id so a retry never posts twice. Each takes the webhook’s events, min_severity, timeout, retry_delay, max_pending and public_url; each speaks https only, follows no redirect and honours the platform’s own 429 wait as the backoff floor; 401/403 hold the queue, any other 4xx is counted as rejected. See chat notifications.
  • A hostile hostname cannot page a channel. Every title, name and summary line is cleaned of control characters, bounded, and escaped for the platform it goes to — HTML entities for Telegram and Matrix, Markdown escapes for Discord, &<> entities for Slack — in one pass that never splits an entity or exceeds a platform’s cap. Discord’s allowed_mentions is empty; Slack’s header is plain_text.
  • Device names at delivery time. The webhook envelope gained the additive incident.device_names object: the current display name of every device on the incident, looked up when the event is delivered — a manual name first, then the resolved name, then the inventory’s own. Chat messages print those names; a device the inventory does not know is shown by its id. A receiver must keep falling back to device_ids. See the envelope.
  • The instance appears as connector:<instance> with the webhook’s five counters, is unlicensed without the feature and sends nothing then, and ndr connectors test <instance> --live posts a real message marked as a test.

0.29 — Webhook sink and Prometheus metrics (R32, 2026-09-12)

Section titled “0.29 — Webhook sink and Prometheus metrics (R32, 2026-09-12)”

The first outbound path of the appliance, and the first way to watch it from the monitoring you already run. Both are Free.

  • A generic webhook sink. id: webhook in connectors.yaml delivers one signed HTTP POST per event to a receiver you name: a new incident, an incident whose score rose, an incident an operator closed, and every health component that changes state — including the appliance as a whole. The body is the nyxtrace.webhook.v1 envelope with the incident’s list fields and a link into the dashboard (NDR_PUBLIC_URL); evidence and the analyst narrative stay behind the link. Every request carries X-Nyx-Signature, an HMAC-SHA256 over the timestamp and the raw body under a shared secret file. Delivery is at-least-once and in order: events are written in the same transaction as the incident, a receiver’s 401/403 holds the queue until the secret is fixed, 429 honours Retry-After, a 5xx is retried three times, any other 4xx is counted as rejected and skipped, and up to max_pending events wait while the receiver is down. A new instance starts at the head of the log; ndr connectors reset-cursor replays what is still held. ndr connectors test <instance> --live sends a signed test event — the one connector test that writes, because a receiver is proven by receiving. See webhook.
  • Sink counters in the health line. connector:<instance> for a sink shows delivered, failed, rejected, dropped and pending; a sink never becomes stale, and a sink with nothing to send is ok, not unknown. See health monitoring.
  • GET /metrics. A Prometheus exposition of the whole appliance — health component states, per-feed ingest rates, incidents by status and severity, connector states and counters, the licence, the updater and the analyst budget — behind a bearer token from NDR_METRICS_TOKEN, 404 until the token is set, served on the loopback bind and rendered at most every ten seconds. Labels are bounded to tenants, sensor ids, connector instances and closed vocabularies; no device, address, domain or incident id ever becomes a label. See metrics.
  • GET /api/v1/health additionally reports events_last_5m_by_feed; the contract is otherwise unchanged. See endpoints.

0.28 — Resolver verdicts in the device aggregate (R31, 2026-09-12)

Section titled “0.28 — Resolver verdicts in the device aggregate (R31, 2026-09-12)”

The blocked by resolver mark promised in 0.24 never appeared in production, and the cause was wider than a missing flag: the device detail has read the hourly device_context_stats aggregate since R16, and that aggregate did not know the Pi-hole and AdGuard Home records at all — migration 009 had only widened the dns_events view, which the detail no longer reads. A device’s Domains (DNS) panel therefore showed none of the resolver’s questions, not merely no verdict.

  • Migration 010 adds a blocked state to the aggregate and rewrites its materialised view in place so the two resolver kinds reach it. It is ALTER TABLE … MODIFY QUERY, the statement 0.23 introduced and granted — no new grant, no drop, no gap for ingest writes.
  • One operator step. The rewritten view only sees records that arrive after it ran, so ndr backfill-stats --days 30 has to run once after the restart, while the service keeps serving. Without it the panel shows the resolver’s names and verdicts only from the upgrade on — never a wrong one. See migration 010 and the one-time backfill.
  • resolver_blocked on the API’s domains rows is now populated from the aggregate; the contract is unchanged. See endpoints.
  • The demo data marks one domain per device as refused by the resolver, so the mark can be seen without a resolver connector.

0.27 — Connector hardening (R30, 2026-09-12)

Section titled “0.27 — Connector hardening (R30, 2026-09-12)”

Six findings of a security review of the connector chain, each fixed with a regression test. No configuration changes.

  • A resolver cannot stall its own feed. A Pi-hole or AdGuard Home query stamped more than five minutes ahead of the appliance clock is refused and counted as failed, and the instance turns degraded with that reason until a poll refuses nothing; the cursor never moves past the present, and a bookmark an earlier release let run into the future is rewound once. A new connector state, degraded with a reason, in the system view and the health API, and a new command, ndr connectors reset-cursor <instance>, to start any instance over (stop the service, reset, start). See a resolver with a wrong clock.
  • A back-dated lease cannot merge devices. The first-seen of a DHCP lease anchor is bounded to the lease’s own lifetime or the 24-hour identity gap, whichever is longer, and never more than seven days back; the raw lease_start is still reported. See DHCP leases.
  • A guest name is a label or nothing. A Proxmox VE guest name — or a NetBox dns_name — with a control character in it, or over 200 bytes, names nothing; the MAC anchors of the guest are recorded as before. See Proxmox VE.
  • Inventory text is bounded. Every text field a connector writes into the inventory is checked for valid UTF-8, stripped of control characters, trimmed and capped before it is stored — 128 bytes per field, 200 for a name, 253 for a DNS name, 2,048 for a URL, at most 16 tags of 64.
  • The device detail degrades instead of failing. When the raw-event contexts cannot be read, GET /api/v1/devices/{id} answers 200 with the inventory, statistics and score, empty context blocks and the additive warnings: ["telemetry_unavailable"] instead of a 503; such an answer is not cached. ndr migrate now recreates a replaced view that went missing between its drop and its create. See endpoints and upgrades.
  • One TLS transport. The certificate pin and the optional client certificate are one framework transport for every HTTPS connector, so the DHCP leases connector no longer carries its own copy.

0.26 — The heartbeat’s update report (R29, 2026-09-12)

Section titled “0.26 — The heartbeat’s update report (R29, 2026-09-12)”
  • The daily heartbeat reports what the updater knows about itself, as an optional update object: policy, channel, running version, when the release channel was last asked, the one-word result of the most recent check or attempt, and the ids of the release keys — every value from a closed vocabulary, no free text; the updater’s error text and history stay on the appliance. A Nyxtrace server older than 0.3.0 refuses the object with a 400, and the appliance then resends the same heartbeat without it at once and withholds the report for 24 hours before trying again — a mixed fleet costs one log line a day, never a day without heartbeats. ndr telemetry show prints update report: included, none (no updater state) or withheld until …. See the update report.

0.25 — Proxmox VE as an inventory source (R27, 2026-09-12)

Section titled “0.25 — Proxmox VE as an inventory source (R27, 2026-09-12)”

Which VM is this address? answered by the system that runs the VM.

  • The Proxmox VE connector. A read-only API token with privilege separation and the built-in PVEAuditor role reads every virtual machine and container of a cluster: the guest list, the virtual NICs from each configuration (MAC, bridge, VLAN tag) and — through the QEMU guest agent or the container’s network namespace — the addresses those NICs carry right now. Only an address on one of the guest’s own NICs counts; a docker bridge inside the guest does not. Disks, CPU and cloud-init are never decoded. The node certificate is pinned by fingerprint; plain http is refused. Free. See Proxmox VE.
  • A new naming source, hypervisor. The guest name ranks third — after a manual name and NetBox, above the UniFi controller map — and is shown as via hypervisor. Nine sources now, see device naming.
  • Identity anchors from the hypervisor. The MAC of every virtual NIC, its vendor prefix and the guest name as authoritative_map, under source = connector:proxmox.
  • The guest in the device detail. A Proxmox block under the NetBox block shows virtual machine or container, name, VMID, node, status, pool, tags, operating system and the instance, with Open in Proxmox VE; GET /api/v1/devices/{id} returns the same as the additive proxmox object, null unless the hypervisor knows one of the device’s addresses. See devices and endpoints.
  • Health: connector:<instance> carries guests, running, ips, agent_ok, templates and nodes on every poll; a poll over 2,000 guests is truncated. A guest without the guest agent is inventoried without addresses — not an error.
  • The connector was built against the public API documentation and synthetic fixtures; it has not yet been exercised against a live cluster by the project itself.

0.24 — Pi-hole, AdGuard Home and DHCP leases (R24–R26, 2026-09-12)

Section titled “0.24 — Pi-hole, AdGuard Home and DHCP leases (R24–R26, 2026-09-12)”

Three Free connectors for the network that has no OPNsense to log from, and the device detail that shows what they know.

  • Pi-hole and AdGuard Home as DNS feeds and naming sources. Every query with the resolver’s verdict lands in the same DNS events Zeek and Unbound produce, under source: pihole / adguard and trust firewall_app; a name the resolver refused is marked blocked by resolver in the device detail’s domain list and carried as resolver_blocked in the API. The clients the resolver knows become device names: dhcp for a lease the resolver holds, resolver for everything else — a new naming source ranked after mDNS and before reverse DNS, shown as via resolver. Pi-hole speaks the v6 API with an app password and the v5 form with the API token (api_version: auto); AdGuard Home uses a dedicated basic-auth user. Both are Free. See Pi-hole and AdGuard Home.
  • The DHCP leases connector. Reads the DHCP server’s lease store — Kea’s memfile CSV, ISC dhcpd.leases or the Kea Control Agent — and turns each active lease into the device name (via DHCP, valid until the lease ends) and into identity anchors whose window is the lease: MAC, DHCP host name, client identifier, vendor class. The file modes open no network connection; --live reads the file as the service user. Free. The OPNsense recipe copies /var/db/kea/kea-leases4.csv over a restricted SSH key and is documented as untested against a live OPNsense. See DHCP leases.
  • The live lease in the device detail. A DHCP block under the NetBox block shows hostname, MAC, vendor class, client identifier, Lease until and the instance; GET /api/v1/devices/{id} returns the same as the additive dhcp object, null unless a lease still binds. See devices.
  • Migration 009 replaces the dns_events view so it admits the resolver’s records and adds the columns source and blocked. It needs the DROP VIEW grant of the updated ClickHouse users file, which must be in place before the restart — see migrations.
  • Health: connector:<instance> carries queries, blocked, clients for a resolver and leases_active, leases_expired, leases_released, leases_declined, hosts_named for a lease source. A resolver poll that needed more than twenty pages is truncated and drops the oldest queries of the window.
  • The certificate pin moved from the UniFi connector into the framework, so every HTTPS source uses the same one; disabling verification is still not offered.

0.23 — Signed automatic updates, NetBox follow-ups (R21, R23, 2026-09-12)

Section titled “0.23 — Signed automatic updates, NetBox follow-ups (R21, R23, 2026-09-12)”
  • The signed auto-updater. An appliance can install its own releases: it fetches an Ed25519-signed release manifest, verifies it against its own release keyring, checks the signed SHA-256 of the bundle, snapshots the current state, installs through the bundled installer, runs the migrations, restarts and rolls back on its own if the appliance is not healthy within 120 seconds. Off unless the operator sets a policy and enables the timer; the default policy is notify. ndr update --check|--apply|--history| --rollback and an Update now button on the system view. The check sends the version, the channel and a truncated hash of the install id used only for the rollout bucket. See upgrades.
  • Migration 008 rewrites the two destination views in place with ALTER TABLE … MODIFY QUERY, so the external-destination predicate is the same text in the summary tables, the backfill and the classifier. It needs the ALTER VIEW MODIFY QUERY grant of the updated users file, which must be in place before the restart.
  • NetBox follow-ups. The device search matches the NetBox device name and the dns_name of an address; a NetBox record the connector has not re-emitted for three full snapshots (72 h) expires — the netbox object becomes null and the name is shown as a former name — instead of describing an object that left NetBox. dns_name is part of the netbox object. See NetBox.

R22 — NetBox as a naming and inventory source (2026-09-12)

Section titled “R22 — NetBox as a naming and inventory source (2026-09-12)”
  • The NetBox connector. A read-only Pro source against the NetBox REST API reads devices, virtual machines and IP addresses every 15 minutes and turns them into device names and inventory context. The device or VM name becomes the device name directly after a manual rename — name_source: netbox, shown as via NetBox — and role, site, tenant, description and status appear in the device list (a role line under the name) and in the device detail (a NetBox block). See NetBox.
  • No configuration change needed. An appliance that already has NDR_NETBOX_URL and NDR_NETBOX_TOKEN for the link lookup gets the connector declared from those two variables, without connectors.yaml and without NDR_CONNECTORS_ENABLED. A declared file entry wins over the environment.
  • The token needs exactly three view rights — ipam.ipaddress, dcim.device, virtualization.virtualmachine — and the test connection proves them.
  • The connector:netbox health line carries NetBox’s own counters (devices, virtual_machines, ips), which move even when a poll found nothing changed; the instance is stale after three missed polls.
  • The device API returns a netbox object with the record, or null.
  • Route validation errors are now English throughout.
  • Every address has a class, and broadcast, multicast and link-local are never “external”. An operator found 255.255.255.255 on the situation view as an external endpoint with an unknown country; the cause was a single test — “no inventory device knows this address” — used everywhere. One classifier now decides internal, private_unmapped, broadcast, multicast, link_local, loopback, unspecified, reserved, documentation, external or unknown, and only external reaches the globe, the country list, the arc groups, the external-endpoint list and the destinations page. GeoIP is no longer asked about anything else. See address classes.
  • Network map: local broadcast, multicast and link-local groups are hidden by default; the Show local noise switch shows them, with the count.
  • Device detail: those counterparts are one line — Local broadcast/multicast traffic with the volume and the protocols (mDNS, SSDP, LLMNR, …) — instead of a list of group addresses posing as destinations.
  • Detectors: beaconing, scan and the rarity destination features ignore local noise. A television waking up is no longer a new external destination.
  • Graph nodes and both ends of a flow carry address_class and address_label; the device detail carries local_noise. All additive.

R19b — Analyst enablement documented (2026-09-12)

Section titled “R19b — Analyst enablement documented (2026-09-12)”
  • The optional OpenAI analyst has an operator page: the required variables (NDR_ANALYST_PROVIDER=openai, OPENAI_API_KEY, NDR_ANALYST_MODEL, the daily budget and the two per-million-token rates in micro-USD, output cap, timeout), how the budget is reserved and settled, the strict JSON schemas, the licence feature analyst, and the fact that no test ever calls a paid API. No product change; the documentation caught up with what R5 shipped.

R19 — Licence gates re-evaluate live; threat-intel refresh (2026-09-12)

Section titled “R19 — Licence gates re-evaluate live; threat-intel refresh (2026-09-12)”
  • A licence change takes effect without a restart. R18’s connector gate evaluated once, before the first online activation had finished, and kept a Pro connector unlicensed until a manual intervention. The gates now subscribe to the licence state: an activation, a renewal or a revocation starts or stops the Pro connectors, the response executor and the analyst within seconds.
  • Managed threat-intelligence refresh. A daily timer builds /etc/ndr/threat-intel.yaml from Spamhaus DROP v4/v6 and the abuse.ch Feodo Tracker, SSLBL and URLhaus feeds, validates every row, refuses an oversized or malformed candidate and installs only a completely valid file — the last good file stays otherwise. The threat-intel health line reports the result. Spamhaus DROP is free for non-commercial use only. See threat intelligence.
  • The NetBox link lookup (netbox_url on the device detail) is live as an optional inventory provider with its own netbox health line.
  • One version package for the binary, the bundle and the licence client; ndr telemetry show includes the licence state.

R18 — Online licence and telemetry live (2026-09-11)

Section titled “R18 — Online licence and telemetry live (2026-09-11)”
  • The appliance activates its licence key against the key server, verifies Ed25519 entitlement tokens under certificate pinning, and renews them in the background; the 72-hour grace behaviour, the license health line and the License section of the system view are what licensing describes.
  • Instance registration and the daily heartbeat run against the live server.

R17 — External destinations (2026-09-12)

Section titled “R17 — External destinations (2026-09-12)”
  • A destinations page. Destinations in the navigation ranks the external addresses your devices talked to in the current window — by bytes, flows or devices — with country and network, the top protocols and the last sighting. Filters for country, network (ASN), protocol, source VLAN and a text search; everything is in the URL, so a filtered view is a link. See external destinations.
  • A detail sheet per destination: traffic per hour, the complete protocol and port breakdown, every internal device that reached it, the incidents of those devices and their model results.
  • Names come from what the client said. The name of an external address is the TLS server name or the HTTP host header observed against it, labelled as such. The appliance resolves no reverse DNS and never implies a DNS record.
  • A Top destinations tile in the overview’s connections sheet, reading the same endpoint over the same window as the page.
  • Two summary tables and a backfill (ndr backfill-stats) make the ranked list answer in well under a second over the whole retention; a byte total still being merged is marked with a tilde rather than presented as exact.
  • Device detail, graph and health reads moved from full scans of the flow table onto pre-aggregated hourly tables filled by the ingest path, with a backfill for the retained history. A device detail that took several seconds answers in a fraction of one; nothing about what it shows changed.

R15 — Analysis catch-up, state-store contention, PTR diagnosis (2026-09-11)

Section titled “R15 — Analysis catch-up, state-store contention, PTR diagnosis (2026-09-11)”
  • The hourly analysis run no longer fails on a resumed checkpoint; a backlog is caught up automatically over successive runs, and the checkpoint advances only after the results are durable.
  • The state store serialises writers and batches them, so the device API stays responsive under ingest load.
  • Reverse-DNS naming distinguishes no name (NXDOMAIN) from an error and reports both counts in its log line; the installer no longer waits on a prompt when re-applied without a terminal.
  • No blue left in the interface. The accent orange now both operates and designates: buttons, controls, links and focus states use it, instead of borrowing the design system’s blue for anything that is clickable. Rule D-6 of the product design system was replaced rather than bent, and the docs site follows the same rule — including its focus ring.
  • Hover and active states are darker and lighter grades of the same orange, with near-black text on filled controls, so a control keeps its contrast without a second hue.
  • Severity and score remain coloured type. No bars, no badges, no glows.

R13 — UniFi live, speaking names, telemetry

Section titled “R13 — UniFi live, speaking names, telemetry”
  • The UniFi Network connector is live. The read-only source against a UniFi OS controller runs on the real scheduler: it detects UniFi OS and classic controllers, pages through clients while de-duplicating by MAC address, and writes identity anchors with source = connector:unifi-network. Its certificate is pinned by fingerprint, and there is still no option to disable verification.
  • ndr connectors validate, test <id> --live and collect <id> --once --live prove reachability, authentication, authorisation and the response shape from the command line before the service ever polls — and --live is separate from the frozen-recording path, which remains the unit of testing. Every declared instance gets a connector:<id> health line in the system view.
  • Speaking device names. A device is no longer named after its address. An hourly job resolves the name to show from six sources in a fixed priority — operator, controller, DHCP, mDNS, reverse DNS, address — and the interface says which source it came from. The name is an overlay: nothing that already reads a device name had to change, and an operator rename wins over every automated judgement. On by default; NDR_NAMING_ENABLED=false switches it off.
  • The telemetry client. Every installation now registers itself and sends one small daily heartbeat: version, operating system, plan and bucketed counts, with a field whitelist and a pattern guard that abort the send rather than trim it. The System page shows the exact payload, ndr telemetry show prints it without sending, and NDR_TELEMETRY=off switches the whole thing off on the free plan. See instance registration and telemetry.
  • Secrets for connectors have two accepted shapes and nothing else: mode 0640 owned by root with the service group, or mode 0600 owned by the service user. Everything else is refused at validation with the file name and the fix.
  • The entire interface is English. The translation layer introduced in R11 was removed; every visible string now sits directly in the component that renders it. There is no language switch.
  • Dates, times and numbers use a 24-hour clock with the day before the month, and byte units remain binary (MiB, KiB) because that is how the backend counts.
  • Route names, query parameters and API error codes were deliberately not renamed. They appear in shared links, in the identity-provider callback and in redirects; renaming them is a migration, not a translation.
  • The interface adopted the Kumo design system — the open-source design system of the Cloudflare dashboard — as dark Nyxtrace tokens. Colours, spacing and text sizes now come from one place, which is why a repaint is a change to one file rather than a renovation of every component.
  • The shell, cards, tables, controls and the login page were rebuilt on it.
  • Text base size is 14px, the density of a dashboard rather than a document.
  • The accent orange is reserved for the brand and for exactly one product state — live — while controls use the design system’s own operating colour. Superseded in R14, which gave the controls the orange as well.
  • Severity and score stay coloured type, never bars, badges or glows.

Both behind feature flags, off by default, so an existing installation keeps its behaviour until an operator switches them on.

  • Device identity, phase one. A device is an identity cluster of weighted anchors rather than a MAC or an IP address. An hourly job reads DHCP events and mDNS names, clusters them, and writes proven host names and MAC addresses back onto the inventory. A merge keeps the earlier first-seen timestamp, so a rotating device does not restart its learning phase. Merges are recorded immutably and are individually reversible; a reversal blocks that anchor combination for that pair only.
  • Addresses now resolve at a point in time, so a reassigned address no longer inherits another device’s history.
  • The device detail shows the anchors and the merge history, with a marker for a randomised address and for an anchor class that can only confirm an identity.
  • The connector framework. One shape for every integration: sources, sinks and responders, with configuration validation, secret references, health, rate limits, backoff and a test connection owned by the framework rather than by each connector.
  • A shared conformance suite every connector must pass, and frozen, anonymised recordings so no connector test needs a live foreign system and continuous integration needs no network.
  • The first connector: a read-only UniFi Network source with a pinned certificate fingerprint and no option to disable verification.
  • ndr connectors list | validate | test, none of which opens a network connection.
  • The incident strip keeps its height and shows a proper empty state when there are no incidents, instead of collapsing the layout.
  • The tenant badge is hidden in single-tenant mode.
  • Sidebar and network-status overlaps fixed.
  • A full-surface situation view with the globe, floating navigation, and an incident rail sorted by score.
  • The incident dialog is URL-stable: an incident is a link you can send to someone, and the browser’s back button behaves.
  • A separate 2D network dialog, with edge and action side sheets.
  • Single-tenant mode, which hides the tenant control entirely rather than showing a selector with one entry.

The first complete appliance: the data path, the storage schema, the detection models, the incident workflow and the first interface.

  • A native Go core owning normalisation, flow correlation, detection, the incident workflow and every database write. The earlier prototype implementation was removed entirely.
  • ClickHouse for telemetry and evidence; a local state store for inventory, workflow, checkpoints and budgets.
  • Transport is transport. The collector reads, buffers and forwards; it assigns no tenant and normalises no fields. Identity comes from the source token.
  • Correlation across overlapping sensors that never sums counters — one preferred observation supplies the numbers, every other observation stays as evidence.
  • The detection model set: rarity, beaconing, robust hourly baselines, scan detection, peer-group isolation, and one composite model.
  • A 14-day learning phase during which model results are recorded and deliberately suppressed.
  • The optional analyst, off by default, with a required explicit budget and explicit token rates — no guessed prices.
  • Response proposals with a required rollback plan. No execution.
  • The native appliance installer with checksummed artifacts, six separate mode-0600 secret environments, a repeatable apply, and a fail-closed partial state.
  • A local health check, Grafana health dashboards on a restricted read-only database account, and an alert rule that ships paused.
  • Optional local GeoIP city and network attribution, with independent readers so one database remains usable without the other.

Everything before R5 was design and prototype work, and is not described as a release.