


Executive Summary
On 11 September 2026 Jonathan Johnson published the first public reverse-engineering look at the Windows Endpoint Security Platform (WESP) as it shipped in Windows Insider Preview build 29661: wesp.sys (kernel minifilter) and espclient.dll (user-mode client). No public SDK yet; symbols were enough for a working consumer POC. The political context is the CrowdStrike outage of July 2024, Microsoft’s Windows Resiliency Initiative (November 2024) and the June 2025 update that promised a private preview. CrowdStrike worked with Microsoft; Alex Ionescu showed a Fal.Con implementation. Johnson’s post is the public binary peek: how consumers talk to the driver over \EspFilterPort, who is allowed to connect (WESP://Permission and Antimalware PPL), how process-create events flow through a rule engine into a user-mode queue, a POC that actually prints events, and a TraceLogging provider in the client DLL.
A crash in the user-mode consumer does not, by itself, bring down the box which was the ultimate goal of this project.
Jonathan Johnson, 11 September 2026
Introduction: after CrowdStrike, Microsoft keeps the kernel
The July 2024 CrowdStrike kernel-driver outage made “should vendors still ship kernel components?” a Microsoft question, not a conference question. WRI’s pitch was more capability outside the kernel without stripping vendors. Private preview to partners the month after the June 2025 blog. CrowdStrike in the room. Ionescu’s Fal.Con talk had a working WESP consumer. Build 29661 is the first public drop of the two binaries. No SDK. Symbols. Johnson reversed enough of both to register a client and receive process-creation events. Code snippets in the post are trimmed on purpose — not 1:1 with the binaries.
This draft keeps every original figure and listing in order, then adds the kitchen picture, a permission-class table, and what hunters should log once this leaves Insider.
WESP architecture: kernel and user-mode
Consumer/producer. Microsoft owns the producer: wesp.sys, a driver/minifilter. Vendors own consumers: they register, create an event queue, install rules whose actions point at that queue, and get matching events on callbacks in a normal process. The driver still registers the OS callbacks and packages data. Parsing, detection, and response live in user mode. Driver-quality burden shifts to Microsoft. A consumer crash is not a bugcheck.
The Esp prefix is everywhere. Exports already look like an SDK that has not been published:

Johnson got a minimal POC working from those exports plus symbols and an AI assistant. That is the rest of the post.
| Piece | Mode | Job |
|---|---|---|
| wesp.sys | Kernel minifilter | Collect, evaluate rules, deliver via filter port |
| espclient.dll | User-mode | Register, queue, rules, FilterGetMessage, ETW |
| \EspFilterPort | Filter comm port | Up to 512 connections; ACL + WESP://Permission |
| PersistedStore | Registry | HKLM\…\Services\wesp\PersistedStore\Clients\<guid> |
How consumer and producer talk
wesp.sys creates a filter communication port named EspFilterPort with FltCreateCommunicationPort, max 512 connections — the same pattern minifilters have used for years:
UNICODE_STRING portName = RTL_CONSTANT_STRING(L"\\EspFilterPort");
FltBuildDefaultSecurityDescriptor(
&securityDescriptor,
FLT_PORT_ALL_ACCESS);
InitializeObjectAttributes(
&objectAttributes,
&portName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
nullptr,
securityDescriptor);
status = FltCreateCommunicationPort(
Filter,
&ServerPort,
&objectAttributes,
ServerContext,
fltmgr::connect_callback,
fltmgr::disconnect_callback,
fltmgr::message_notify_callback,
512);
Connect/disconnect/message callbacks. New connections hit fltmgr::connect_callback. FilterSendMessage from user mode hits message_notify_callback.
Who is allowed to connect?
The port has an ACL. FltBuildDefaultSecurityDescriptor with FLT_PORT_ALL_ACCESS still means administrators and SYSTEM on the default SD. After that, wesp::server::connect reads the caller’s primary token for a security attribute named WESP://Permission. It is not a boolean. It is one of two unsigned 64-bit values, shown in decimal:
| WESP://Permission (decimal) | Test signing off |
|---|---|
| 10000000 | Does not require Antimalware PPL |
| 1000000000 | Requires Antimalware PPL (protection level 0x31) |
For 1000000000, WESP queries ProcessProtectionInformation and checks Antimalware PPL (0x31). Familiar: a lot of vendor capability on Windows is already PPL-gated. On this preview build with test signing enabled, Johnson connected from an elevated test process without the token attribute and without AM-PPL. Port ACL still applied (admin/SYSTEM). He reads that as a developer convenience Microsoft left in on purpose.
After checks, server::connect reads context from espclient.dll: registration, unregistration, management, or event-queue delivery. That state is what disconnect and message callbacks use later.
When do these checks happen?
On every new connection to \EspFilterPort: register, unregister, enumerate — each opens a short-lived connection. EspConnectClient opens the longer management connection and pays the check once. Later FilterSendMessage on that handle does not re-query token or PPL; the driver checks whether that connection may perform the operation. Event receive is a second connection via EspConnectEventQueueWithCallback, its own check, then FilterGetMessage. Individual messages do not re-check. Close and reopen, checks run again.
How events move from producer to consumer
The product pitch: EDR-like visibility without each vendor shipping another kernel driver. WESP collects process, thread, image load, file, registry, handle — then sends events that match a client’s rules to user mode.
Process creation as the walkthrough. Init registers create_process_notify with PsSetCreateProcessNotifyRoutineEx2:
status = PsSetCreateProcessNotifyRoutineEx2(
0,
create_process_notify,
FALSE);
Non-null PS_CREATE_NOTIFY_INFO is create; null is terminate. Metadata goes into the rule engine:
if (CreateInfo != nullptr)
RuleEngine::process_event<ProcessCreate>(..., &event);
else
RuleEngine::process_event<ProcessTerminate>(..., &event);
Before delivery: EspCreateEventQueue → message_notify_callback → ClientObject::create_event_queue. Then EspConnectEventQueueWithCallback with client and queue GUIDs. WESP starts a delivery worker with PsCreateSystemThread waiting on a KEVENT via KeWaitForSingleObject when empty.
Consumer: EspAllocateEventNotification, EspArmEventNotification — posts async FilterGetMessage and waits. On process create, matching ProcessCreate rules name a queue. EventQueue::queue_async_notification_internal enqueues and signals the KEVENT. Worker dequeues and FltSendMessage into the pending FilterGetMessage. Callback runs, EspCompleteEventNotification, rearm.


Building a WESP consumer POC
No SDK. espclient.dll exports were enough. Johnson published WespConsumerPOC: register, queue, ProcessCreate rule, enough output to prove events arrived. Full ProcessCreate layout still unknown, so not all metadata is printed. Everything goes through the DLL — register/clients/remove and monitor. The DLL owns \EspFilterPort.
.\wesp-consumer.exe register
.\wesp-consumer.exe clients
.\wesp-consumer.exe monitor "{CLIENT-GUID}" 60
.\wesp-consumer.exe remove "{CLIENT-GUID}"
Once registered, monitor:
- EspConnectClient — existing registered client.
- EspCreateEventQueue — kernel queue.
- EspConnectEventQueueWithCallback — user-mode callback on that queue.
- EspAllocateEventNotification + EspArmEventNotification — first receive.
- EspCreateRule — local ProcessCreate rule whose action points at the queue.
- EspUpdateRules — install for the connected client.
- Match → queue → FltSendMessage → FilterGetMessage → callback → EspCompleteEventNotification → rearm.
Queue-before-rule is the footgun. The rule does not hold the callback. It describes the event and points at a queue. The callback belongs to the queue’s delivery connection. Arm receive before installing the rule so the consumer is already waiting.

Woo! Events. Remove persisted objects with:
.\wesp-consumer.exe remove "{FCB4EF81-F69B-4979-ADA2-C7BCF7B73792}"
Registering writes HKLM\SYSTEM\CurrentControlSet\Services\wesp\PersistedStore\Clients\

He did not spend more time on the key. Client APIs already enumerate and remove registrations, which was enough for the POC.
WESP ETW visibility
One TraceLogging provider in espclient.dll: Microsoft.Windows.WESP.Client. Queue, rule, connection — success and failure. Johnson found it with Get-EtwProviders from ETWInspector:
$providers = Get-EtwProviders `
-ProviderType TraceLogging `
-FilePath C:\Users\TestUser\Desktop\espclient.dll
$providers.TraceloggingProviders
FilePath Providers
-------- ---------
C:\Users\TestUser\Desktop\espclient.dll {Microsoft.Windows.WESP.Client}
The provider and event set are also in his EtwWatcher snapshot for 10.0.29661.1000. Preview schema will move. Example EspCreateEventQueue event:

Wrapping up
Johnson’s tone: fun, first public binaries, moving out of the kernel is closer than people thought. The long pole is not the tech — it is enterprises upgrading. Eighteen months from WRI to public wesp.sys is fast. Plenty left: prevention rules (CrowdStrike showed blocking malware in Fal.Con), the rest of the event types, the SDK. Watch other reversers, especially Yarden Shafir.
Resources
- Windows security and resiliency (Nov 2024)
- WRI June 2025 update
- FltBuildDefaultSecurityDescriptor
- FltCreateCommunicationPort
- FilterConnectCommunicationPort
- User-mode / minifilter communication
- WespConsumerPOC
- ETWInspector
Why this is not “EDR is leaving the kernel”
wesp.sys is still a kernel driver. PsSetCreateProcessNotifyRoutineEx2 is still a kernel callback. File and registry still go through a minifilter. What left the kernel is the vendor’s parser and the vendor’s bugcheck. That is a huge operational win (CrowdStrike-class outages become user-mode crashes) and a huge trust shift (Microsoft now owns the only collector; vendors see what WESP chooses to package). If WESP’s rule language cannot express a detection, the vendor either lives with the gap or keeps a driver. Johnson flags prevention rules as the next public fight.
PPL gating is the other half of the political design. Antimalware PPL is how Windows already decides who is a “real” AV. Tying WESP://Permission 1000000000 to 0x31 means the lobby is not open to every admin tool. The 10000000 class without PPL is the interesting preview knob — a vendor-shaped permission that is not the full uniform. Watch whether that class survives GA.
A researcher’s map of the remaining surface
- Full event structs (ProcessCreate and the rest) — Johnson could not print all fields.
- Rule language and prevention actions (Fal.Con malware block).
- File/registry/image/handle paths through the same rule engine.
- PersistedStore ACL vs WinTcb — who can read vendor GUIDs without EspEnumerate.
- Whether test-signing bypass of Permission/PPL remains in later builds.
- ETW schema drift as the preview client DLL moves.
- Yarden Shafir and others as the preview widens.
Hunting WESP before it is GA
On 29661: Microsoft.Windows.WESP.Client, \EspFilterPort connections, services\wesp in the SCM, PersistedStore keys (even if you cannot open them, creation is a signal), wesp-consumer.exe if someone cloned the POC. A non-PPL process holding a long-lived Filter communication handle to EspFilterPort on a production-looking image is either Insider test signing or a story.
A glossary so the rest of the RE stays readable
| Term | Kitchen | Operator |
|---|---|---|
| WESP | Microsoft’s new mail-sorting room for endpoint events. | Windows Endpoint Security Platform; wesp.sys + espclient.dll on 29661. |
| Producer / consumer | Sorting room vs detectives in the lobby. | Driver collects; vendor process consumes. |
| EspFilterPort | The slot in the wall. Max 512 people in line. | FltCreateCommunicationPort; FilterSendMessage / FilterGetMessage. |
| WESP://Permission | Badge sticker, not a yes/no stamp. | Token attribute: 10000000 or 1000000000 (decimal). |
| AM-PPL 0x31 | The real uniform. | Antimalware Protected Process Light; ProcessProtectionInformation. |
| Event queue | The window number on your ticket. | EspCreateEventQueue; rule action points here, not at a callback. |
| PersistedStore | A locked filing cabinet even the building manager cannot open. | HKLM\…\wesp\PersistedStore\Clients\ |
Filter ports are not new. Putting all EDRs behind one is.
FltCreateCommunicationPort, FilterGetMessage, FltSendMessage, default SD for admin/SYSTEM — minifilter textbooks. What is new is that this port is meant to be the only kernel I/O path for a generation of vendors. 512 connections is a capacity number, not a security number. Each register/unregister/enumerate is a short connection; EspConnectClient and EspConnectEventQueueWithCallback are the long ones. Two long handles per consumer is the mental model: management vs delivery. Mixing them is how you confuse the driver’s “is this connection allowed to do X” check.
The access-check timing Johnson mapped matters for both testers and hunters. Token and PPL are sampled at connect, not at every FilterSendMessage. A handle stolen after a legitimate AM-PPL process connected is a different bug class than “can I open the port as admin.” Preview test-signing skipping Permission entirely is a third class. Write those three sentences on the whiteboard before you argue about whether WESP is “locked down.”
Queue-before-rule is the API’s personality
People coming from ETW or minifilter callbacks expect “subscribe to ProcessCreate, here is my function pointer.” WESP splits that. A rule is a description plus an action that names a queue. The callback is a property of the queue’s delivery connection. That is why Johnson armed FilterGetMessage before EspUpdateRules. If you install the rule first, events can match while nobody is listening, and you will swear the API is broken. It is not. You subscribed a mailbox after the letters started moving.
The same split is how one client can have many queues and many rules without stuffing callbacks into the rule blob. It is also how a future prevention rule might point at a different action than “enqueue” — CrowdStrike’s Fal.Con block demo is the teaser. Johnson did not reverse that path. Do not invent it.
PPL, WinTcb, and a registry key that says no
Antimalware PPL is 0x31. WinTcb is higher. Johnson ran a SYSTEM prompt as WinTcb and still could not open PersistedStore. That is not a bug report; it is a product statement: the list of registered WESP clients is not a trophy for whoever can enable PPL. Enumeration goes through EspEnumerateRegisteredClients, which pays the port checks. If you are writing IR for “which EDR is registered with WESP,” that API (and ETW) is the supported answer. Direct registry is a dead end on this build, which is useful information by itself.
The 1e7 vs 1e9 permission class is the other PPL story. 1e9 is “you are AM-PPL.” 1e7 is “you have the attribute but we will not demand the uniform.” On a locked-down enterprise that 1e7 class is either a lab gift or a confused vendor identity. Demand Microsoft document it before you let a non-PPL binary hold a management connection next to Defender.
What a crash in the lobby actually buys
CrowdStrike 2024 was a kernel driver with a bad content update. WESP’s design goal, in Johnson’s sentence, is that a crash in the consumer does not bugcheck the machine. That is true only for logic that moved to user mode. wesp.sys can still bugcheck. A bad rule evaluation, a bug in packaging, a filter communication failure — those are now Microsoft’s. Vendors cannot ship a sensor.sys that takes the box with them, which is the point. Vendors also cannot patch wesp.sys on their own clock. That is the trade.
Enterprises will live on old Windows for years. Johnson says the long pole is upgrades, not the API. Until 29661-class OS is the floor, you will have kernel EDR and WESP consumers on the same estate. Plan coexistence, not a flip.
ETW as the public SDK until the SDK exists
No headers, no lib, no samples from Microsoft. The TraceLogging provider Microsoft.Windows.WESP.Client is the part you can subscribe to without reversing. Queue create, rule update, connect success/fail — Johnson’s EspCreateEventQueue screenshot is the template. EtwWatcher’s 29661 snapshot is a time capsule; the schema will move. Version the provider in your SIEM the way you version Defender ETW. Get-EtwProviders against espclient.dll is how you notice a new event name after the next flight.
If you clone the POC tomorrow
- Insider 29661 (or later with the same two binaries). Test signing on if you are not AM-PPL.
- Elevated admin. Filter port ACL is still admin/SYSTEM.
- register, clients, monitor “{guid}” 60, then start notepad and watch.
- remove the GUID. Confirm PersistedStore is gone via the API, not via regedit.
- Throw the VM away. Do not leave a registered client on a laptop you care about.
The printed ProcessCreate fields are incomplete because the struct is not public. Do not treat missing command-line or missing parent PID in the POC as “WESP does not have them.” Johnson said he does not know the full layout. Absence of evidence in a weekend POC is not evidence of absence in the driver.
Key Takeaways
- WESP public in Insider 29661: wesp.sys + espclient.dll. No SDK. Symbols enough for a process-create consumer.
- Microsoft collects in kernel; vendors consume in user mode. Consumer crash ≠ bugcheck. That is the CrowdStrike lesson encoded in a minifilter.
- \EspFilterPort, 512 connections, admin/SYSTEM ACL, then WESP://Permission 1e7 vs 1e9 (AM-PPL 0x31). Test signing on this build skipped both for elevated testers.
- Queue then rule. Callback is on the queue connection. FilterGetMessage / FltSendMessage is the path.
- PersistedStore denies even WinTcb SYSTEM. Use the client APIs. ETW: Microsoft.Windows.WESP.Client.
- POC on GitHub. Preview will change. Watch prevention rules and Yarden.
Defensive Recommendations
- Do not treat 29661 as production WESP. Treat it as a map.
- Inventory who will get WESP://Permission and AM-PPL in your estate; that is the new EDR install identity.
- Plan ETW on Microsoft.Windows.WESP.Client the way you plan Defender/vendor ETW today.
- If you still run third-party kernel EDR, WRI is the migration story — the long pole is OS upgrade, not the API.
- Lab the POC. Confirm test-signing behavior on each new Insider flight; file it if the skip survives too long.
- Read the registry ACL on PersistedStore as a sensitivity label: vendor GUIDs are not for everyone with a kernel debugger habit.
Conclusion
Eighteen months after Microsoft asked whether vendors should still live in the kernel, the first public WESP binaries answer: the collector stays, the detectives move to user mode, the port is a filter communication port you already know how to reverse, and a weekend with symbols is enough to print process creates. The interesting arguments — prevention, PPL classes, what the rule engine cannot see — are still behind the private SDK. Johnson opened the curtain. The play is not over.
Original text: “A First Look Inside the Windows Endpoint Security Platform” by Jonathan Johnson at jonny-jhnson.dev.


