# 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.

In numbers: 4 Microsoft providers on one session, 0 kernel drivers, 0 detections
from nine read-only admin commands, confidence 0.55 to 0.90 as the command line
corroborates, and 2 open defects named here.

## 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
(/docs/kill-switch) and The /128 identity (/docs/wire-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=21` and the rendezvous counter
  shows `kernelNotesSeen=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:

```
ETW real-time session · WhisperSensorETW
(4 Microsoft manifest providers, narrow keyword masks)
        |
        v            WINDOWS-ONLY · thin
event callback:
   filter by (provider, event id) BEFORE any decode
   decode wanted events · bounded hand-off (16,384 records, shed-newest, counted)
        |
        v            OS-NEUTRAL · unit-tested on Linux CI
property bag -> typed record
   (field-for-field reshape; a record with no subject is
    dropped, never invented)
        |
        v            OS-NEUTRAL · unit-tested on Linux CI
cross-platform envelope · exec / file / conn / dns / image-load
        |
        v
process graph · local NDJSON spool
```

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 /all` scored 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.exe` at 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%\System32` and 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=0` alongside 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)
scoop bucket add whisper https://github.com/whisper-sec/scoop-bucket
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:

```
whisper service install --sensor
whisper service start --sensor
# or both at once
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 to `C:\Program Files\Whisper`,
  writes a machine-wide config under `C:\ProgramData\Whisper`, registers the service and
  starts it. `uninstall.ps1` reverses 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
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.
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:

```
sc query whisper-sensor
logman query "WhisperSensorETW" -ets
whisper service status --sensor
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 (/docs/platforms) 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.
