WhisperDocs
Endpoint

Telemetry collectors

Every collector the sensor ships, what each watches, the kernel mechanism it rides, and the blind spot it declares. The default build is pure Go and GPL-free; the heavier kernel taps are opt-in. Collectors are visibility. The verdict layer that reads them lives in Detection engines.

What runs by default

Two steps, not one, and deliberately so. The plain one-liner installs the signed binary and starts nothing: a server that only wants a resolvable identity is never surprised by a new collector appearing on it.

$ curl -fsSL https://get.whisper.online | sh

Ask for the sensor and it starts with every collector on:

# curl -fsSL https://get.whisper.online | WHISPER_SENSOR=1 sh

That installs the binary and a dedicated whisper-sensor unit, running immediately. From there nothing needs enabling one by one: every collector in the table below, and every detection engine that reads them, ships on by default. The default build carries no cgo and no GPL object. Its core collectors are the exec collector on the kernel NETLINK process-connector, the file collector on fanotify, the outbound and inbound socket collector on NETLINK_SOCK_DIAG, and the DNS attribution tap on AF_PACKET. A critical-config integrity watch and a vulnerability-posture collector ship in the same pure-Go build. A lower-overhead eBPF exec and per-connect tap is a separate opt-in build behind -tags ebpf, which links a GPL-2.0 object and is never in the public MIT tree.

i

Kernel-graceful, fail-open. A collector that cannot attach on a given kernel is dropped, and the sensor keeps running on the rest. It never fails the host. The default exec path needs no eBPF, so it runs on kernels well below 5.3.

One stream, four vocabularies, forward-first

Everything the collectors see rides one telemetry stream in four vocabularies: exec, file, conn and dns, plus a fifth detection kind that engine findings ride back on. The rule that shapes the whole sensor is forward-first: the hot path always forwards an event downstream before any detector looks at it. Detectors attach either as taps spliced inline that forward first and fold a judgement on the side, or as observers that side-consume copies and by construction cannot delay, change or lose a single event.

The forward-first river: four kernel taps feed one stream that always forwards downstream; detection engines hang off the side and can never delay, drop or alter telemetry. KERNEL TAPS exec · NETLINK file · fanotify conn · SOCK_DIAG dns · AF_PACKET ONE STREAM · forwards first, always spool → backend never blocked taps: Sigma · YARA-X · chains · ransomware · injection a tap folds its judgement ON THE SIDE; findings ride back as a fifth `detection` kind detection can never delay, drop or alter telemetry
The forward-first river. Four vocabularies, one stream, and the stream always flows: a slow or wedged detector loses its own judgement, never your telemetry.

The stream is also defended at the source. Both netlink collectors accept only datagrams from the kernel itself (source port-id 0) and bind each dump to a fresh sequence number, so a local attacker cannot fabricate exec or connection records to bury the real ones. The exec collector carries a PID-recycle guard: if /proc turns out to be newer than the exec event it is enriching, it drops the stale identity rather than attribute the wrong binary. The collector set behind this page carries 2,112 test functions and 93 fuzz targets across 216 test files.

The collectors, at a glance

CollectorWatchesMechanismPlatformStatus
Process execevery execve, /proc-enrichedNETLINK proc-connectorLinuxLive · default
Process exec (deep)same, lower overhead + earliereBPFLinux ≥ 5.3Opt-in · -tags ebpf
File changescreate / modify / deletefanotifyLinuxLive · default
Settled-hash laneSHA-256 of settled new executablesfanotify CLOSE_WRITE + bounded drainLinuxLive · default
Critical-config watchwrites to sensitive pathsfanotify + closed path allowlistLinuxLive · default
Sockets, both directionsoutbound connect + inbound acceptNETLINK_SOCK_DIAGLinuxLive · default
Per-connect net-tapeach new connection at SYN_SENT, source porteBPFLinux ≥ 5.3Opt-in · -tags ebpf
DNS attributionqname to originating processAF_PACKET + classic BPF filterLinuxLive · default
Inventory + posturepackages, open CVEs, SBOM-litedistro metadataLinuxLive · default
Windows ETWexec / file / network telemetryETWWindowsLive · telemetry
macOS ES bridgeEndpointSecurity + NetworkExtension eventsES daemon over NDJSONmacOSLive · entitlement-gated

Process exec

The exec collector is the spine of the sensor. By default it reads the kernel NETLINK process-connector, a pure-Go path with no cgo and no GPL, and enriches each execve from /proc (argv, exe path, uid, ppid) with the PID-recycle guard described above. The image's SHA-256 and its known-good verdict are computed asynchronously, off the hot path, so enrichment never slows the stream. The opt-in eBPF build sees execs earlier and at lower overhead, and links a GPL-2.0 object, so it is kept out of the public MIT tree and turned on deliberately with -tags ebpf. Either way the event shape is the same. This is the raw stream behind the measured 100% telemetry run:

[T1003.008 read /etc/shadow]  {"comm":"cat","filename":"/usr/bin/cat","pid":1658,"ppid":1656,"uid":0}
[T1547.006 kernel module]     {"comm":"insmod","filename":"/usr/bin/kmod","pid":1875,"uid":0}
[T1027 obfuscation]           {"comm":"base64","filename":"/usr/bin/base64","pid":1986,"uid":0}
[T1071.001 C2 user-agent]     {"comm":"curl","filename":"/usr/bin/curl","pid":2018,"uid":0}
[T1105 ingress transfer]      {"comm":"rsync","filename":"/usr/bin/rsync","pid":7495,"uid":0}

On that run the exec collector captured 17,092 exec events with zero drops, including one scan loop that alone spawned 10,974 processes. The full matrix is in ATT&CK coverage.

File changes, the settled-hash lane and the critical-config watch

The file collector rides fanotify for create, modify and delete, resolving paths via file handles rather than racy name lookups. Two lanes sit on top of it.

The settled-hash lane: when a new ELF or script closes for write, its settled content is digested on a bounded side lane and a SHA-256 hash annotation joins the stream. That annotation is what feeds the YARA scanner and the known-good hash verdicts downstream. The lane is bounded by design: a hashing storm sheds digests, never the drain, so a mass file-drop can cost you some hashes but never your file telemetry.

The critical-config integrity watch carries a closed, hard-coded allowlist of 16 classic Linux persistence and privilege-escalation paths, each pre-tagged with its ATT&CK technique: the dynamic-linker preload files, the cron directories, systemd and init service paths, the sudoers files and the SSH daemon config. It marks parent directories so a file that does not exist yet (a fresh /etc/ld.so.preload) is caught at creation, filters every non-allowlisted sibling, and refuses to follow symlinks so a link-swap race cannot redirect the watch. The write is captured the moment it happens, not inferred after the fact:

# the config-watch collector, on a critical file write:
[T1574.006 ld.so.preload]  {"kind":"file","event":"create","path":"/etc/ld.so.preload","critical":true}

Sockets, the net-tap and DNS attribution

Network visibility is three collectors, each answering a different question, and each declaring what it cannot see.

The three network taps, each labelled with its kernel mechanism, what it sees, and its declared blind spot. SOCK_DIAG standing sockets, v4+v6 direction: dialed vs accepted unprivileged, pure Go blind: a connection that opens and closes inside one tick SYN_SENT · eBPF, opt-in the instant a dial starts carries the srcPort flow key observe-only by design blind: nothing; it exists to close the sub-tick gap AF_PACKET · udp/53 the DNS question, per PID kernel-filtered before bind 64 attributions/sec budget blind: DoT and DoH bypass port 53, simply not observed one tee → the per-endpoint graph: process → name → peer
Three taps, one rule: what a tap cannot see, it never claims. The periodic socket dump misses sub-tick connections, which is exactly the gap the opt-in per-connect tap closes; the DNS tap states plainly that encrypted resolvers bypass it.

The DNS tap deserves its mechanics spelled out, because it is the rarest edge any EDR draws and it uses no resolver plugin and no eBPF. A classic BPF filter is attached before the socket binds, so the kernel only ever delivers outgoing UDP datagrams to destination port 53; the sensor never sees unrelated traffic. The DNS question is decoded by a bounded, total parser (RFC-1035 255-octet name cap, at most 8 compression jumps), and the querying process is recovered post-hoc: the datagram names the querying socket by source port, the source port maps to a socket inode via one bounded /proc/net/udp scan, and the inode maps to a PID through a one-second-TTL reverse index. Attribution is budgeted at 64 lookups per second; past budget, or when the socket is already gone, the event degrades to PID 0, a held-but-unattributed node, never a lost edge and never a /proc churn storm. The tap watches exactly the hop that carries the real querying process (the stub-resolver hop to systemd-resolved or dnsmasq), and it says plainly what it cannot see: DoT and DoH bypass port 53 and are simply not observed.

Why carry a source port on a DNS event at all? Because real tooling splits the resolve and the connect across different processes (a shared system resolver asks, the payload process dials). The shared ephemeral port is the flow key that lets the backend fuse the resolve and the connect back together across that split. Together the three taps power the write-exec-connect event chain and the exfil-fusion join, folded into the per-endpoint graph; the three-graph story they enable is in How the sensor works, and what the verdict layer makes of them is in Detection engines.

Inventory and vulnerability posture

The posture collector enumerates the host's own packages, its open CVEs and an SBOM-lite from distro metadata. Collection is self-contained: no heavy client-side SBOM upload. The graph then ranks the findings by KEV, EPSS, CVSS and known-ransomware use, so a fleet of thousands of CVEs distills to the handful worth fixing first. On a real box:

1,285
packages
4,002
open CVEs
12
KEV, fix first

That run surfaced real Known-Exploited entries including Log4Shell (CVE-2021-44228). The collector is read via op:list{kind:'posture'}.

i

The gate. The posture collector is live. The graph-side CVE severity scoring (the KEV and EPSS ranking) runs against a pre-production corpus today and promotes to the production graph on a dated gate, so treat the ranking as beta while the collection itself is solid.

What heavy work costs, and when it runs

Collection itself is cheap by construction; everything that is not cheap (full-file hashing, deep scans, inventory sweeps) is deferrable work handed to the calm scheduler, which admits it only when the host is idle, on workers at nice 19 with idle I/O priority, under the sensor's own cgroup ceiling. Starvation floors guarantee that a fresh unknown executable is still examined within about a minute even on a busy host. The measured whole-sensor footprint is roughly 2.6% of one core and 16 MB; the scheduler, the floors and the ceilings are in Calm footprint & SLA.

Windows and macOS

The deep on-host sensor runs on Linux and Windows today, and macOS already emits telemetry through the same wire contract.

Windows ETW is live: one real-time session over four manifest providers carries exec, file, connection, DNS and image-load telemetry, a pure-Go user-mode collector with no kernel driver, and the ransomware, dropper-chain and process-injection engines run over those streams on-host, proven on a real Windows Server 2022 host. The ETW-TI tier is built but dark, gated on Microsoft's anti-malware attestation, not on engineering; the Vulnerable-Driver blocklist matcher is wired unconditionally and live, registered alongside the rest with no config knob of its own.

macOS ships an EndpointSecurity and NetworkExtension bridge that ingests the native ES daemon's events over an authenticated local socket, sharing the same Go core and a byte-identical event contract proven against the Linux golden corpus. Customer ship is gated on the Apple EndpointSecurity entitlement, so it is internal and dev today, not a shippable tier. The full matrix is in Platform support.

i

Whatever the OS, identity, resolution and the off-host kill-switch already cover any host or agent on the network. A device that will never run a kernel sensor (a PLC, a camera, a contractor laptop, an ephemeral AI agent) still gets a verifiable /128 and can be contained from the network with no agent installed.

Where the telemetry surfaces

Collector output becomes a queryable per-endpoint census, enriched with a trust tier and an affirmative known-good verdict so the analyst sees the calm majority, not just red. The API key stays server-side; the read is op:list{kind:'proc'}:

$ curl -s https://graph.whisper.online/api/query \
    -H 'X-API-Key: whisper_live_...redacted' \
    -H 'content-type: application/json' \
    -d '{"query":"CALL whisper.agents({op:\"list\", kind:\"proc\",
             address:\"2a04:2a01:8dae:4521:45b8:b241:4f91:56e2\", limit:500})"}'

envelope: op=list  ok=true  status=200  elapsed_ms=66
processes returned : 500
hash-verdict known-good (fleet allowlist) : 52
known-good on THIS host                   : 54
carrying a signal                         : 62

# sample rows (host web-edge-01; known-good rides the signal list)
exe=/usr/bin/wg    trust=T3  knownGood=true  onHost=true  signals=[hash-verdict:known-good,hash-verdict:known-good-onhost]
exe=/usr/bin/ip    trust=T3  knownGood=true  onHost=true  signals=[hash-verdict:known-good,hash-verdict:known-good-onhost]
exe=/usr/bin/bash  trust=T2  knownGood=true  onHost=true  signals=[fan-out:8,sensor-rep:32,hash-verdict:known-good]
Whisper EDR console hunt page showing an op:list result of about 320 process rows across the fleet, with a note that the API key stays server-side.
The same collector telemetry, queried in the operator console hunt surface (about 320 process rows). The key never leaves the server. Real capture, demo fleet.
i

Telemetry is not a verdict. Everything on this page is visibility: the sensor sees the event. Turning that stream into an analytic verdict that alerts is a separate layer, documented in Detection engines, and we keep the two strictly apart.