The Windows sensor
Every capability sentence on this page comes out of a transcript from one live Windows Server 2022 Standard guest, and the transcripts are quoted as they landed. Where a run proved something, the finding is here verbatim. Where a run found a defect in our own code, the defect is here too, with its FAIL, because a page that publishes only the passes is not evidence, it is advertising. The sensor itself is one user-mode process reading Event Tracing for Windows over four Microsoft providers, with no kernel driver anywhere. On that stream sit the behavioural correlators for ransomware, credential access and defense evasion, plus a Sigma evaluator that carries community rules and names their authors on the wire.
One host, one Windows version, a few dozen deliberate triggers. That is the whole sample, so there is not a percentage anywhere on this page and there is no comparison to anybody else's product. Two defects are open and both are named below, with the runs that found them.
How to read the transcripts
Two conventions run through every run below, and they are worth more than any individual finding.
A zero is only worth reading if something in the same run is non-zero. A detector that is starved and a detector that is correctly silent produce exactly the same empty spool. So every zero claimed here is paired with a count from the same path that proves the events actually arrived. The credential-access run says so in its own verdict header:
VERDICT (asserted by name; every zero has a same-path non-empty control)
[PASS] NEG control: the lone dump-FILE leg reached the spool
[PASS] NEG: a lone dump-file leg is SILENT (corroboration is demanded)
Every claimed improvement is a differential, not an assertion. Where a fix is claimed, the same trigger is also run against a build without it, on the same box inside the same sitting, and both numbers are kept.
Two more things belong up front. The lab guest's host name is redacted as [redacted] in every quoted line; nothing else in a quote is altered. And the transcripts split into two kinds of run: the detection runs drove the sensor binary itself, while the raw telemetry runs drove a separate proving tool that constructs the same collector. Which is which is stated at each number.
Scope. The network-layer half of Whisper, the routable identity, the off-host revoke and the resolution policy, has its own pages: Containment and The /128 identity. Nothing on this page is evidence for those, and none of them depends on this sensor.
The sensor: one ETW session, four providers, five streams
The Windows sensor is the same whisper binary every other platform ships, observing through the operating system's own telemetry facility. It opens one real-time session with a fixed, recognizable name, WhisperSensorETW, so an operator can find it and stop an orphaned one with stock logman. That name is not a design document, it is what the runs check for; every re-proof ends by asserting the session is gone:
==== POST-RUN ====
leftover sensor procs: 0
orphaned ETW session: False
Into that session it enables four public Microsoft manifest providers, each with a narrow keyword mask, so only the events the sensor consumes are delivered:
| Stream | Provider | Events consumed | Keyword scope |
|---|---|---|---|
| exec | Microsoft-Windows-Kernel-Process | ProcessStart (1) | the PROCESS keyword only |
| image-load | Microsoft-Windows-Kernel-Process | ImageLoad (5); ImageUnload (6) filtered before decode | the IMAGE keyword only |
| file | Microsoft-Windows-Kernel-File | Write (16), DeletePath (26), RenamePath (27), CreateNewFile (30); Create (12), Cleanup (13), Close (14) and NameDelete (11) feed the FileObject-to-name cache | per-op keywords: create, write, delete-path, rename-path, filename, file-io. Read and directory-enumeration traffic is never enabled |
| conn | Microsoft-Windows-Kernel-Network | outbound TCP connect (12 v4, 28 v6) | IPv4 and IPv6 keywords only |
| dns | Microsoft-Windows-DNS-Client | query started (3006) | whole provider, it is low volume |
A subscribed provider still emits far more than that, and the callback discards the rest before any decode work. You can watch both halves at once, because a debug flag prints a per-provider, per-event tally at close. This is the real tally from a 45 second proving run, abridged to the five vocabulary events out of the 43 provider and event pairs it listed:
whisper-etw: observed (provider,event) counts (dropped=0 faults=0):
provider=1 event=1 count=21 <- Kernel-Process ProcessStart
provider=1 event=5 count=755 <- Kernel-Process ImageLoad
provider=2 event=12 count=7142 <- Kernel-File Create, the name-cache seed
provider=2 event=16 count=4485 <- Kernel-File Write
provider=3 event=12 count=2 <- Kernel-Network connect, IPv4
provider=4 event=3006 count=2 <- DNS-Client, query started
Read dropped=0 faults=0 with the counts beside it: nothing was shed and no decode panicked, in a run that saw tens of thousands of events. That pairing is the whole point of printing the tally.
- DNS is seen at the question, not the answer. The tap is the query-started event, which carries the name and type before any response exists, so a query that never resolves is still observed.
- The file stream carries create, modify and move, proven by name. Kernel-File names most operations by an opaque FileObject rather than a path, so the collector keeps a bounded FileObject-to-name cache seeded at Create and settled at Cleanup and Close. A burst run wrote 2,500 files and renamed 1,250 of them, then counted how many of those names came back.
wrote 2500 files in 00:00:04.5267385
renamed 1250 files in 00:00:02.9083176
...
==== CONTROL: burst files actually named in the telemetry ====
total NDJSON lines: 10087
file lines naming a b790tally path: 10003
lines naming a t<N>.dat burst file: 10000
lines naming a renamed .locked file: 1250
---- sample (first 3 burst file lines) ----
{"kind":"file","ts":1787831469965070,"pid":1772,"event":"create","path":"C:\\...\\churn\\t0.dat","host":"[redacted]"}
{"kind":"file","ts":1787831469965206,"pid":1772,"event":"modify","path":"C:\\...\\churn\\t0.dat","host":"[redacted]"}
{"kind":"file","ts":1787831469967849,"pid":1772,"event":"create","path":"C:\\...\\churn\\t1.dat","host":"[redacted]"}
1,250 renames produced exactly 1,250 lines naming a renamed file. That run drove the proving tool rather than the shipped service, so read it as a statement about the collector, which is the same one the sensor constructs. The delete verb is wired and subscribed, but no run in this set exercised it, so this page does not claim it.
Command lines for short-lived native processes
Every command-line matcher below stands on this, so it gets its own section. A short-lived native Win32 tool can exit before its command line can be read, and the older build simply had none for those processes. The same script fired the same triggers against a fixed build and a pre-fix build. Both arms matched on the process name, so the record appears in both and only the field differs:
HEAD [exec] pid=4408 comm=wevtutil.exe cmdline=wevtutil cl Application
CONTROL [exec] pid=6968 comm=wevtutil.exe cmdline=<EMPTY>
HEAD [exec] pid=1268 comm=rundll32.exe cmdline=rundll32.exe C:\Windows\System32\comsvcs.dll, MiniDump 0 "...\bogus.dmp" full
CONTROL [exec] pid=3720 comm=rundll32.exe cmdline=<EMPTY>
The cmd.exe parents are the stronger half of that result. The harness filter matches on process name, image path or command line, and no filter term is a substring of cmd.exe or of its image path, so a cmd.exe record can only surface by matching its recovered command line. Two of them appear in the fixed arm. None appears anywhere in the control:
HEAD [exec] pid=6340 comm=cmd.exe cmdline="C:\Windows\system32\cmd.exe" /c "wevtutil cl Application"
CONTROL (no cmd.exe record present)
Three honest notes belong with that.
- The classic kernel source delivered one note per process start, exactly. The same run's tally shows
provider=1 event=1 count=21and the rendezvous counter showskernelNotesSeen=21. Not a partial stream, and nothing waited out its grace or was flushed unfilled:expired=0 drained=0 refusedClosed=0. - Every recovery paid the wait. The same counter reads
filledAtOffer=0 filledParked=11. The fast path that fills with no added delay did not execute once in that run, so all eleven recoveries sat through the park. That is the feature's real cost and it belongs in the open, not in a footnote. - Nothing was truncated, and the truncation marker was working. The harness appends a literal marker whenever the record's truncation flag is set. It had a live way to appear and did not, across the run's seven recovered records, the longest of them 275 characters.
Both arms ran at Mandatory Label\High Mandatory Level, so privilege is controlled for and cannot explain the difference between them. A real-time ETW session needs Administrator or SYSTEM, and this is where you see that requirement being met.
One binary, three layers, one envelope
Under the providers sits a three-layer split, and only the first layer is Windows code at all:
The reshape layer carries decode rules an analyst can lean on, and one of them is visible in the transcripts above rather than only in the source:
- A process start that cannot name its own PID is dropped, never emitted as a PID-0 ghost.
- The started process's own PID property is used, never the event header's PID of the creator, so a child's exec is not attributed to its parent.
- A command line the OS event did not carry stays empty, never invented. That is the literal
cmdline=<EMPTY>in the control arm above. The field went blank instead of being filled with a guess, which is exactly why the fixed build's recovery is legible as a change.
The transcripts on this page all end at the local NDJSON spool, because that is where the evidence ends. No run here shows a finding leaving the host, so this page makes no claim about a console, an API or graph enrichment.
Deep detection, on this box, in these runs
The correlators are the same OS-neutral code the Linux build runs, judging the same envelope. What follows is what they did on this host.
Defense evasion: the command line decides, not the executable name
Six trigger commands, run twice back to back on the same box: once against the shipped build, once against the same tree built without the Windows breadth matcher. On the shipped build, four detections, and the confidence climbs as each new command line corroborates the last:
{"kind":"detection","ts":1787842775274534,"technique":"T1562","pid":3960,"comm":"auditpol.exe","confidence":0.8,
"signals":["evasion:clear_logs","impair:disable_logging","impair:disable_protection","evasion:total_scope","t1562.002"],
"host":"[redacted]"}
{"kind":"detection","ts":1787842777295126,"technique":"T1562","pid":2892,"comm":"vssadmin.exe","confidence":0.9,
"signals":["evasion:clear_logs","evasion:destroy_recovery","impair:disable_logging","impair:disable_protection",
"evasion:total_scope","t1562.002","t1490"],"host":"[redacted]"}
-> 4 detections, escalating 0.55 -> 0.80 -> 0.90 -> 0.90.
The corroborating signal is read out of the actor's own arguments. evasion:total_scope fires off the literal /category:* in auditpol /set /category:* and the /all in vssadmin delete shadows /all, never off the fact that a process is called auditpol.exe.
The same six triggers on the control build, which saw all six execs with their full command lines:
-> 1 detection, 0.55, two classes. auditpol / vssadmin / fsutil arrived with
full command lines and produced NO class at all.
So the breadth matcher is not what introduced coverage of this technique; the control still fired once at 0.55. What it adds is the escalation and the extra classes, impair:disable_logging, evasion:destroy_recovery and evasion:tamper_forensics.
Then the false-positive run: nine read-only and restorative administrator commands, using the same executables as the firing ones.
wevtutil qe Application wevtutil el auditpol /get /category:*
vssadmin list shadows netsh advfirewall show fsutil usn queryjournal C:
Get-MpPreference (read) Set-MpPreference -DisableRealtimeMonitoring $false
sc query WinDefend
-> spool lines 16; exec records present for all of wevtutil(2), auditpol(1),
vssadmin(1), netsh(1), fsutil(1), MpPreference(2), sc.exe(1);
DETECTIONS: 0.
The exec counts are the control for that zero.
The exec counts are what make that zero readable. The executables that appear in the firing run produced nothing at all here, so the discrimination lives in the command line.
One more thing that run showed, working in both directions inside a single transcript. The first detection carries an advisory signal, admin_maintenance_baseline, for a shape that looks like ordinary maintenance. It is graded rather than screened out, and it carries no weight: the detection holding it scores 0.55, byte for byte what the control build scored without it. The next detection drops the advisory the moment /category:* corroborates. An advisory that changed the score would be a thumb on the scale; this one is demonstrably not.
The engine is on by default. A later re-proof asserted exactly that against a config with no defense-evasion field present at all:
ASSERT default-ON: a T1562 detection landed with no defenseevasion field = True
The run left the box as it found it, which is stated rather than assumed:
defender realtime enabled : True
firewall Domain/Private/Public : True / True / True
audit Logon/Logoff : Success and Failure
leftover sensor processes : 0
A false positive we found in our own detector, and the differential that proves it gone
Reading a file's timestamps is not tampering with them. An older build could not tell the two apart, so a pair of harmless timestamp reads inside an already-firing process tree added a forensic-tampering class that did not belong there. The re-proof runs the same reads twice, against the fixed build and against the pre-fix binary:
ASSERT t1070.006 ABSENT on HEAD = True (tamper ABSENT = True)
{"kind":"detection","ts":1787849686122550,"technique":"T1562","pid":5376,"comm":"powershell.exe","confidence":0.8,
"signals":["evasion:clear_logs","evasion:tamper_forensics","impair:disable_protection","evasion:total_scope","t1070.006"],
"host":"[redacted]"}
ASSERT the FP is real on the control: t1070.006 PRESENT here = True
An absence is only meaningful if something else in that run was present, so the same leg asserts the tree fired on its real classes first, and a fourth scenario checks the fix did not simply blind the detector. A timestamp setter in the same shape still convicts:
[PASS] D guard intact: the SETTER still carries t1070.006 on HEAD
Sigma community rules, with their authors on the wire
The event-log clearing conviction in those runs is not ours, and the finding says so itself. The rule id, the title and the upstream author list ride in the signal set:
{"kind":"detection","ts":1787849591640810,"technique":"T1070.001","pid":6160,"comm":"wevtutil.exe","confidence":0.8,
"signals":["sigma_match","rule:cc36992a-4671-4f21-a91d-6c2b72a2edf5",
"title:Suspicious Eventlog Clearing or Configuration Change Activity",
"author:Ecco, Daniil Yugoslavskiy, oscd.community, D3F7A5105, Swachchhanda Shrawan Poudel (Nextron Systems)"],
"paths":["\\Device\\HarddiskVolume3\\Windows\\System32\\wevtutil.exe"],"host":"[redacted]"}
That id resolves to a real rule file in the shipped bundle, and two more community rules fired in the same capture set, one for process memory dumping through comsvcs.dll and one for suspicious rundll32 activity, each with its own id and authors. Attribution is on the wire because presenting a community rule's work as proprietary detection would be a lie by omission.
Credential access: corroboration is demanded, and order does not matter
Four legs, each with its own control. First the negative: a lone dump-shaped file, no tool and no module load anywhere in the tree.
lone dump-file leg captured (control) = True; T1003.001 = 0
[PASS] NEG: a lone dump-file leg is SILENT (corroboration is demanded)
The file leg reached the spool and the detector still said nothing. Then the two orders, which convict either way round:
[PASS] A: the tool-first order CONVICTS T1003.001
[PASS] B: the file-first / tool-last order ALSO convicts (order-insensitive)
The file-first fold is the interesting one. It correlates across a parent and child process edge, names the parent shell that actually wrote the dump, and lists both the dump and the tool in its evidence:
{"kind":"detection","ts":1787851908770968,"technique":"T1003.001","pid":340,"confidence":0.75,
"signals":["lsass_dump_tool_unconfirmed_target","lsass_dump_file_created"],
"paths":["C:\\Users\\Administrator\\w824reprove\\b\\watch\\evidence-b.dmp",
"\\Device\\HarddiskVolume3\\Windows\\System32\\rundll32.exe"],"host":"[redacted]"}
The run also corrected the premise it was filed under. rundll32 loading comsvcs.dll for a MiniDump was expected to need a dump file to corroborate it; it does not, because the module load itself is corroboration, at 0.9:
[PASS] MOD (premise corrected): MiniDump 0 FIRES - it is NOT uncorroboratable
[PASS] MOD: it fires via the comsvcs.dll load (lsass_dump_module_loaded), no file needed
Read the signal name before you read the technique id. Every one of these findings carries lsass_dump_tool_unconfirmed_target, and the name is literal: the target in these runs was a decoy process, never a real credential store. What is proven is that the correlator convicts the dumping shape from two independent legs, in either order, and stays silent on one leg alone. Detection of a real credential theft is not what these transcripts show.
Ransomware: the tripwire, the arithmetic, and a defect that is still open
The sensor plants its own decoy files in each watched directory, five of them, 131 bytes each, sorted to sit at both ends of a directory walk:
==== planted canaries (the sensor's own decoys) ====
Name Length
---- ------
!important-passwords-backup.docx 131
0-crypto-wallet-keys.txt 131
ledger-records-7464.pdf 131
mortgage-summary-5424.xlsx 131
zzz-tax-and-bank-records.xlsx 131
canary count: 5
Three of those names are fixed strings in the shipped engine and two are derived per host, which is why a decoy list an attacker greps out of the binary does not cover the whole set. The true positive, a canary touch correlating with a mass-write burst on one process, reproduced across two independent runs at different PIDs:
{"kind":"detection","ts":1787831158722911,"technique":"T1486","pid":5420,"comm":"powershell.exe","confidence":1,
"signals":["canary","mass_write"],
"paths":["C:\\Users\\Administrator\\whisper-e2e\\b790head\\churn\\!important-passwords-backup.docx",
"C:\\Users\\Administrator\\whisper-e2e\\b790head\\churn\\b2499.dat",
"C:\\Users\\Administrator\\whisper-e2e\\b790head\\churn\\b2498.dat",
"C:\\Users\\Administrator\\whisper-e2e\\b790head\\churn\\b2497.dat"],"host":"[redacted]"}
That confidence of 1 is not a probability and not a model score. It is a capped sum of fixed weights: the canary signal carries 0.95 and mass-write carries 0.35, which is 1.30, clamped to 1. The firing threshold is 0.80. The weights are constants in the engine, the same on every platform, and the arithmetic is worth knowing before anybody reads a 1 as certainty.
The run that produced that finding also produced two bad ones, and they are the reason this section is long.
The mis-attribution. Windows raises a file write in the context of the thread that issued the request, so an application's own cached write lands under the application, and the cache manager's lazy writer replays the same dirty pages a second later from the System process. The whole 2,500 file burst therefore appeared twice:
file lines by PID
pid=7104 lines=5001 distinct paths=2501 <- powershell, the real writer
pid=4 lines=2509 distinct paths=2506 <- System
That produced a spurious full-confidence finding naming pid 4 beside the correct one. Both pid-4 findings carry no process name at all, because opening the System process is denied, which is what a responder actually sees: an unnamed process they cannot act on. Worse, the earliest pid-4 finding fired 10.8 seconds before the burst, on the sensor's own freshly planted decoys. The sensor was detecting itself.
Two changes, measured across three runs of the identical burst:
before attribution fix + canary flush
file lines under pid 4 2509 10 3
distinct paths under pid 4 2506 6 1
T1486 findings 3 2 1
findings naming pid 4 2 1 0
findings naming the writer 1 1 1
The writer's distinct path count is 2,501 in all three runs, so the recovery changed who the writes belong to and never how many files were seen. The three surviving System lines name the churn directory, not a file, which is the filesystem flushing its own index on a handle System genuinely owns. That is honestly attributed and deliberately left alone: one directory cannot approach the thirty distinct paths the mass-write signal needs, and a directory is never a decoy path. The end state is one finding:
{"kind":"detection","ts":1787842516358554,"technique":"T1486","pid":6852,"comm":"powershell.exe",
"confidence":1,"signals":["canary","mass_write"],
"paths":["...\\churn\\!important-passwords-backup.docx","...\\churn\\b2499.dat",
"...\\churn\\b2498.dat","...\\churn\\b2497.dat"],"host":"[redacted]"}
Those two runs used dedicated proof binaries rather than the shipped one, so read the table as the diagnosis it is. The one corroboration from the shipped build is a single clause in a later run: [PASS] ENC on HEAD: NO detection is attributed to pid 4 (System).
And the defect that is still open. A benign encrypted-vault sync shape, 600 new files each written once, is still convicted as ransomware, and the fix believed to address it changes nothing measurable on this scenario. The run's own verdict is FAIL:
CONTROL: file lines naming vault-<N>.bin create=600 modify=1200 distinct-paths=600
T1486 detections: 1
{"kind":"detection","ts":1787850923234528,"technique":"T1486","pid":3660,"comm":"powershell.exe",
"confidence":0.85,"signals":["high_entropy","mass_write"],"paths":[...],"host":"[redacted]"}
[FAIL] FP on HEAD: ZERO T1486 on the create-only high-entropy burst
REPROVE871: FAIL
The pre-fix control build produced exactly one finding too, at the same 0.85, with the same signal set and the same writer. On this scenario the two builds are indistinguishable. Recall is intact in the same run, which is why the fix is not simply being reverted: overwriting 120 pre-existing files in place still convicts, names the encrypting process, and attributes nothing to System.
Both legs carry spool controls, so the FAIL is a measurement rather than a broken harness.
What these transcripts do not show
This list is part of the evidence, not a disclaimer bolted onto it.
- No rate of any kind. Six defense-evasion triggers, nine read-only commands, four credential-access legs and two ransomware scenarios, on one host. There is no denominator here that could support an accuracy, precision or false-positive rate, so none is published.
- No per-command score table. The same
vssadmin delete shadows /allscored 0.9 in one run and 0.8 in another, because confidence is a function of the whole accumulated signal set in that run, not of the command. - One host. Windows Server 2022 Standard, a single guest. No desktop SKU, no domain-joined host, no second machine, no macOS or Linux comparison drawn from these runs.
- Detection only. Nothing here kills a process, quarantines a file or isolates a host. Every transcript is read-only telemetry ending in a local spool.
- No real ransomware and no real credential theft. The ransomware reproducer writes random bytes into a scratch directory it creates and clears, and is benign by construction. Every credential-access target was a decoy process.
- No performance, overhead or detection-latency figure. None of these runs measures any of them, and the event timestamps in a finding are event times, not measured latencies.
- Two false positives live inside this evidence set. The create-only ransomware conviction above, and a recurring process-injection finding on
explorer.exeat 0.85 that appears in all four scenarios of one run. The second is the expected shape a stock Windows box produces through legitimate runtime code generation, annotated on the wire rather than suppressed. Neither is counted in any tally on this page.
Two open defects
Named here rather than left for a reader to find.
1. A benign create-only high-entropy burst is convicted as ransomware. Detailed above. Verdict FAIL on the current build, identical output on the pre-fix build, with a control proving 600 distinct paths reached the spool.
2. The sensor does not exit on Ctrl+C on the console path. It survives a real CTRL_C_EVENT past the 20 second bound, has to be force-killed, and the shutdown tally and the graph teardown are lost with it:
sensor exited=False after 20.34s (bound 20s)
force-killing (Close did NOT run - the tally below WILL be absent; that is the defect, not a flake)
tally lines present: 0/3; graph-teardown line: False
The cause was isolated by changing one variable. The identical binary, started the identical way, exits in 0.4 seconds on a CTRL_BREAK event with all three tally lines and the teardown line present:
sensor exited=True after 0.4s (bound 20s)
tally lines present: 3/3; graph-teardown line: True
[PASS] L3b ISOLATION: the SAME prefix binary EXITS on CTRL_BREAK in 0.4s
[PASS] L3b so the shutdown path is healthy; the fault was purely CTRL_C delivery
So the shutdown path was healthy the whole time and the fault is purely in how the Ctrl+C event is delivered to a process that inherited an ignore flag from its parent. The consequence is not cosmetic: a force-killed sensor leaves an orphaned ETW kernel session behind, and that orphan was observed contaminating the start of later runs.
The reason this defect is still listed rather than closed is the more useful lesson. A previous round fixed it: the function was written, committed and covered by three passing unit tests. It was never called from the production entry point, so the shipped binary behaved exactly as if no fix existed.
ROOT CAUSE: RestoreConsoleCtrlC() is defined + unit-tested but NEVER
CALLED from the sensor run entrypoint - a shipped-but-unreachable fix.
The mechanism itself is proven: a binary built with the call genuinely wired onto the console path exits in 0.01 seconds on a real CTRL_C_EVENT and passes all eight assertions, against a control from the same tree that fails six of them. The call site now exists in the tree, with a guard test that parses the real entry point and fails if the wiring is removed. But a green guard test is precisely what the previous round also had, and no run on a live box has yet stopped a wired binary with Ctrl+C. Until one has, this page says the defect is open.
No kernel driver, no dependencies
Nothing of ours runs in the kernel. The whole sensor is one user-mode process consuming a standard OS telemetry facility. There is no filter driver, no boot-start component, no driver package in the tree at all, and therefore no driver of ours that can fault a kernel. Its failure domain is one process the Service Control Manager restarts.
The ETW binding is self-contained too. The Win32 tracing and decoding entry points are not covered by Go's standard Windows syscall package, and rather than adopt a third-party ETW library or cgo, the sensor carries its own thin binding:
- System32-only loading. Every entry point resolves lazily from the system DLLs under
%SystemRoot%\System32and nowhere else, so a DLL planted on the PATH cannot hijack the sensor's imports. - Structs verified before use. The Win32 structures the callback touches are mirrored field for field, and every mirror's size is checked against the documented header size before the collector opens a single handle. A layout mistake becomes one clear refusal at construction, never silent memory corruption.
- A session lifecycle that converges. A stale same-named session left by a crashed prior run is stopped and the start retried. That path is not theoretical, it is what handles the orphan the Ctrl+C defect leaves behind, and the runs record it happening:
note: a stale WhisperSensorETW session was present at start (an earlier force-kill); stopping it. - Per-property decode that degrades per field. Each wanted property is pulled by name; an unknown type keeps its raw bytes with no typed view, and a surprising field never fails the whole event. Addresses and ports are read from the raw network-order bytes rather than reinterpreted as integers.
- Pure-Go cross-compile. No cgo and no third-party ETW dependency, so the Windows sensor builds from the same tree, in the same CI, as Linux and macOS.
Bounds and safety: the numbers the runs actually printed
- Filter before decode. The callback compares against the wanted provider and event id pairs before any decode work. The 43-pair tally quoted earlier is the proof that far more arrives than is decoded, and that
faults=0alongside it. - A bounded, non-blocking hand-off, 16,384 records deep. A full channel sheds the newest observation and counts it rather than blocking the tracing thread. The runs print the high-water mark, and it is worth reading honestly. Under the 2,500-write plus 1,250-rename burst it reached
record channel high-water=8710/16384, over half the ring. At ordinary activity it sat at 197 and 201 of 16,384. That is a reading from two activity levels on one host, not a headroom guarantee. - Zero loss on every channel the session counts, in the runs that got a tally.
kernel session eventsLost=0 rtBuffersLost=0 logBuffersLost=0, with the burst counts above as the control that proves those zeros are readings. Note the qualifier: a force-killed run prints no tally at all, so most runs in this set cannot report loss. - The tally is a diagnostic, not default output. All three of those lines print only when a debug environment variable is set, which every harness here sets explicitly. An operator does not see them by default and this page does not pretend otherwise.
- The resource budget is published, and the run says plainly it could not measure itself against it. Verbatim:
whisper: sensor: calm footprint: unmeasured (no cgroup v2 self-accounting) under the published SLA
(cpu 25%, mem 157286400B soft / 262144000B hard); deferrable submitted=15 completed=13 dropped=0
promoted=0 pending=2
150 MiB soft, 250 MB hard, a quarter of a core. On Windows there is no self-accounting to measure against it, so the line says unmeasured instead of printing a flattering number. The deferrable work scheduler shed nothing during the burst, though pending=2 is a snapshot at an abnormal termination rather than a drained steady state.
One clear privilege gate. A real-time session needs Administrator or SYSTEM; the verified runs all show Mandatory Label\High Mandatory Level. A non-elevated run degrades at construction with one line naming which collector this platform lacks, and everything else keeps serving.
Install as a Windows service
The CLI installs from the same channel the rest of the toolchain uses on Windows:
# PowerShell: install the whisper CLI (one static binary, no dependencies)
PS> scoop bucket add whisper https://github.com/whisper-sec/scoop-bucket
PS> scoop install whisper
Then, from an elevated PowerShell, the same cross-platform service manager that drives systemd on Linux and launchd on macOS drives the Windows Service Control Manager:
PS> whisper service install --sensor
PS> whisper service start --sensor
# or both at once
PS> whisper service install --sensor --now
- It runs as LocalSystem, the credential-free account that satisfies the elevation the real-time session requires. No stored username, no stored password.
- Start type automatic, restart on failure, and a reinstall refreshes the definition and restarts the running process so the fresh definition takes effect.
- A bundled
install.ps1, run elevated, copies the binary toC:\Program Files\Whisper, writes a machine-wide config underC:\ProgramData\Whisper, registers the service and starts it.uninstall.ps1reverses it. - The DNS redirect the script can add is opt-in and separate. Observation never depends on it, because DNS is read from the tracing session regardless of which resolver the host uses.
One honest note about all of that. Every transcript on this page ran the sensor as a console process, not under the Service Control Manager. The service path is shipped code, but it is not what these runs exercised, and this page does not claim a result it did not produce.
Verify it is working
The first proof needs no key, no sensor and no trust in us, because it is the network plane's proof and it is the same from any operating system. The example is a live identity, so every line here reproduces. This output was captured by running it:
# reverse DNS names the address, the identity in public
PS> dig -x 2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478 +short
ae3b051ff3bf7f478.tdc38e7c55bad3306a92b830f9bb1e4f9.agents.whisper.online.
# and the name validates to the IANA root, so our API is not in the trust path.
# point delv at a validating resolver; a local stub that strips DNSSEC data
# will report a broken chain that is its own, not the zone's.
PS> delv @1.1.1.1 -x 2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478
; fully validated
8.7.4.f.7.f.b.3.f.f.1.5.0.b.3.e.7.1.7.6.a.9.6.b.1.0.a.2.4.0.a.2.ip6.arpa. 60 IN PTR ae3b051ff3bf7f478.tdc38e7c55bad3306a92b830f9bb1e4f9.agents.whisper.online.
8.7.4.f.7.f.b.3.f.f.1.5.0.b.3.e.7.1.7.6.a.9.6.b.1.0.a.2.4.0.a.2.ip6.arpa. 60 IN RRSIG PTR 13 34 60 ...
The second proof is the running sensor, in stock tooling. These are the commands; this page does not print an expected transcript for them, because the only Windows outputs it publishes are ones a run actually produced:
PS> sc query whisper-sensor
PS> logman query "WhisperSensorETW" -ets
PS> whisper service status --sensor
PS> Get-Content C:\ProgramData\Whisper\logs\whisper-sensor.log -Tail 20 -Wait
The habit worth copying from these runs is the third command's companion. Before you believe any zero the sensor reports, count something in the same spool that must be non-zero:
==== spool accounting (the CONTROL for the zeros above) ====
spool total lines: 7518
file lines: 7510
file lines naming a burst file (b<N>.dat): 7501
---kind breakdown---
7510 file
3 detection
2 dns
2 conn
1 exec
Those five counts reconcile exactly with the total, which is what tells you the spool is whole and not truncated.
Status, and what the evidence for it is
| Capability | Status | The evidence |
|---|---|---|
| ETW telemetry: exec, file, conn, DNS, image-load | live, proven | One session, four providers, a 43-pair observed tally with dropped=0 faults=0, and 2,500 writes plus 1,250 renames counted back by name in the spool. |
| Command-line recovery for short-lived native processes | proven by differential | Same trigger, two builds: full argv on the fixed build, cmdline=<EMPTY> on the control, with cmd.exe records that can only appear via a recovered command line. |
| Defense-evasion classifier (T1562 family, T1490, T1070.006) | live, proven | Four detections escalating 0.55 to 0.90 on argv corroboration, zero detections from nine read-only commands with the exec counts as control, and an A/B against a build without the matcher. |
| Sigma community-rule evaluation | live, proven | Three distinct community rules matched on this host, each finding carrying the rule id, title and upstream authors on the wire. |
| Credential-access correlation (T1003.001) | proven, decoy targets only | Silent on one leg, convicts on two in either order (0.9 tool-first, 0.75 file-first across a parent and child edge). Every finding is tagged lsass_dump_tool_unconfirmed_target; no real credential store was touched. |
| Ransomware correlator (T1486) | live, open false positive | Canary plus mass-write convicts at the capped 1.0 naming the writer, reproduced across runs; in-place encryption of 120 files still convicts. A benign create-only high-entropy burst also convicts at 0.85, and that run's verdict is FAIL. |
| Write attribution under the Windows cache manager | fixed, measured on proof binaries | Findings naming System went 2 to 1 to 0 across three runs of the same burst while findings naming the real writer stayed 1, with the writer's distinct path count unchanged at 2,501. The shipped-build corroboration is one assertion, not the full table. |
| Graceful shutdown on Ctrl+C | open defect | Force-killed at the 20 second bound in every run in this set, losing the shutdown tally and the teardown line and orphaning an ETW session. Root cause isolated; the wiring is in the tree with a guard test; no live run has yet proven it. |
| Process-injection region sweep (T1055) | runs, not proven here | It produced findings in these runs, but only the known baseline shape on a stock host. No injection was detonated, so nothing here shows it catching one. |
| Detections leaving the host | not shown here | Every transcript on this page reads a local spool. No run shows a finding reaching a console, an API or the graph. |
| Kernel-level operation-time injection feed | in the tree, dark | The decode exists, but subscribing needs a Microsoft anti-malware attestation, which is an external gate. It has never run on a box, and no run here exercises it. |
Held to one sentence: on this Windows host, the telemetry, the command-line recovery and the defense-evasion, credential-access and ransomware correlators all fired with their controls beside them, and the two defects the same runs found are named above rather than rounded away. Platform support owns the full matrix and never rounds a status up.
Why the page is written this way. Anybody can print a detection. What is worth reading is whether the run that produced it also proved the zeros around it, kept the failures, and said which numbers came from a proving harness rather than the shipped binary. That is what these transcripts do, and it is the standard the next Windows run on this page will have to meet.