Skip to content

Threat intelligence

Nyxtrace can carry one local list of indicators — domains, addresses and networks — and mark a match on flow, destination, domain and graph records. The list is a file on the appliance; nothing is looked up online at query time.

A match never invents risk. Scores stay incident-derived, and the marker is a fact on the record rather than a score contribution. A list is a hint, not a verdict.

NDR_THREAT_INTEL_PATH in /etc/ndr/ndr.env names a YAML file with three lists. Any of them may be empty; unknown keys are refused.

domains:
- 'bad.example.invalid'
ips:
- '203.0.113.8'
cidrs:
- '198.51.100.0/24'

The loader is strict:

  • at most 100,000 entries across the three lists together — a larger file is refused as a whole,
  • a domain is lower-cased, a trailing dot is removed, and it may not be empty, longer than 253 characters or contain whitespace or slashes,
  • an address must parse; IPv4-mapped IPv6 addresses are unmapped,
  • a network must be in canonical form (198.51.100.0/24, not 198.51.100.7/24).

One invalid entry fails the whole file, and a file that fails to load leaves the provider absent: matches are then false everywhere and the health line says so. The file is read once at process start, so a changed file needs systemctl restart ndr-api (the managed refresh below does that for you).

Matching compares the destination address against ips and cidrs and the queried or observed name against domains, exactly — no suffix matching, no wildcards.

The appliance ships a refresh script and a systemd timer that build that file from four free feeds:

Feed Contributes Terms
Spamhaus DROP v4 and v6 cidrs Free for non-commercial use only
abuse.ch Feodo Tracker ips CC0
abuse.ch SSL Blacklist (SSLBL) ips CC0
abuse.ch URLhaus host file domains CC0
Terminal window
# in /etc/ndr/ndr.env (root-only, mode 0600)
NDR_THREAT_INTEL_PATH=/etc/ndr/threat-intel.yaml
Terminal window
systemctl enable --now ndr-threat-intel.timer
systemctl start ndr-threat-intel.service
systemctl status ndr-threat-intel.service ndr-threat-intel.timer

The installer places the script at /usr/local/bin/ndr-threat-intel-refresh and the two units at /etc/systemd/system/ndr-threat-intel.{service,timer}. The timer fires daily at 02:17 with a random delay of up to six hours, so a fleet of appliances does not hit the feeds at the same minute, and it is persistent: a run missed while the machine was off happens at the next boot.

The oneshot runs as root under a hardened unit (no new privileges, private /tmp, read-only system with /etc/ndr and /run/lock as the only writable paths) and takes a lock so two runs cannot overlap. It then:

  1. downloads the five files over HTTPS with TLS 1.2 or newer, a 10 s connect and 120 s total timeout and two retries each — a failed download aborts the run;
  2. parses every feed strictly: a DROP row must be a JSON object with a cidr, and the row count must equal the metadata record count the feed itself publishes; a Feodo or SSLBL line must be exactly one field; a URLhaus line must be a host-file line (127.0.0.1 or 0.0.0.0 followed by a name) — one malformed line fails the run;
  3. deduplicates, lower-cases the domains, and validates every address, every network (canonical form, no host bits) and every domain label again with a separate parser;
  4. caps URLhaus at 40,000 domains and refuses any candidate with 100,000 or more entries in total — the parser’s own limit;
  5. writes the candidate under /etc/ndr as a staged file and moves it over /etc/ndr/threat-intel.yaml atomically;
  6. restarts ndr-api (try-restart, so a stopped service stays stopped), because the provider is loaded once at start.

Comments and feed metadata are discarded; the file holds indicators only, with a header naming the sources and their terms.

Fail-closed is the property to rely on: a failed download, a malformed row, a count that does not match the feed’s own metadata or an oversized candidate leaves the last good file untouched and the service running on it. Nothing partial is ever installed.

The oneshot’s journal ends with the counts:

Terminal window
journalctl -u ndr-threat-intel.service -n 5
# threat-intel refresh complete: domains=... ips=... cidrs=... total=...

The system view and GET /api/v1/health carry an enrichment component threat-intel: ok when the file loaded, otherwise unavailable with Optional provider is not configured (no path set) or Optional provider is configured but no successful read has been recorded (the path is set but the file did not load — check the journal of ndr-api). Neither state degrades the overall status.

Disable the timer, restore the previous YAML and the NDR_THREAT_INTEL_PATH setting, restart ndr-api, and keep the failed candidate out of /etc/ndr.

Terminal window
systemctl disable --now ndr-threat-intel.timer

The boolean threat_intel_match is on every flow, on each row of a device’s top destinations and domain projections, on the destinations page (list and detail) and on graph records. The interface renders it as a small warning-coloured marker next to the name or address. Absent configuration yields false and never a hit.

Two things it does not do: it does not change a score, and it does not create an incident. If you want a list match to alert, that is a model condition, not a property of the list. See detection models.

The refresh script is one way to produce the file. A file you maintain yourself — an internal blocklist, a vendor feed you are licensed for — works the same way as long as it has the shape above. Leave the timer disabled in that case, or the refresh will overwrite it at 02:17.