heavener: This is what happens when you can't afford EDR licenses~/otter
SearchSearch
Dark modeLight mode<br>Reader mode
!a.isFolder&&!b.isFolder||a.isFolder&&b.isFolder?a.displayName.localeCompare(b.displayName,void 0,{numeric:!0,sensitivity:\"base\"}):!a.isFolder&&b.isFolder?1:-1","filterFn":"node=>node.slugSegment!==\"tags\"","mapFn":"node=>node"}">Explorer
Table of Contents<br>Architecture<br>The Kernel Driver<br>ETW<br>The Process Model<br>The Module System<br>Vendor Coverage<br>SentinelOne<br>Cortex XDR<br>CrowdStrike<br>Sophos<br>The Replay System<br>Alerts and the Console<br>Limitations
heavener: This is what happens when you can't afford EDR licenses<br>A modular engine that runs real vendor detection logic from reverse-engineered EDR components against live or replayed Windows telemetry.
Jun 26, 202620 min read<br>projects<br>edr-internals<br>reverse-engineering<br>windows-internals
heavener is a project I’ve been building for the past 6 months, and it’s probably the most ambitious thing I’ve worked on. It’s a modular EDR emulation engine for Windows that loads real detection logic extracted from commercial endpoint security products I have reverse engineered and evaluates it against live telemetry using the actual vendor artifacts: their ML models used for file classification, their compiled YARA rulesets and behavioral scripts. The user picks a vendor module and gets the exact verdicts the production EDR would produce.
When testing payloads against EDR products, you have two options: run the real agent (and risk burning the payload before you get to actually use it), or try to understand its detection logic well enough to predict its behavior. heavener is the second option taken to its logical extreme.
At the time of writing, four vendor modules are currently implemented: SentinelOne , Cortex XDR , CrowdStrike , and Sophos . Each loads genuine vendor artifacts and runs them through the same interface. The engine collects telemetry, enriches it, and hands it to whichever module is loaded, allowing users to hot-swap modules at runtime through the IPC client without restarting the engine.
Architecture
The system has six major layers connected by a clean data flow. Telemetry comes in from the kernel driver and ETW providers, gets normalized into a uniform event schema, passes through enrichment and correlation, hits the active vendor module, and alerts flow out to a SOC-style web console.
E[DriverConsumer]\n B --> F[EtwConsumer]\n C --> F\n D --> G[ThreatIntelConsumer]\n E --> H[EventPipeline]\n F --> H\n G --> H\n H --> I[ProcessModel + Enrichment]\n I --> J[IEdrModule]\n J --> K[AlertSink]\n K --> L[Forwarder]\n L --> M[Web Console]"">flowchart TB<br>subgraph Kernel<br>A[heavendrv minifilter]<br>end<br>subgraph ETW<br>B[Kernel providers]<br>C[AMSI / WMI / LDAP / DNS]<br>D[ETW Threat Intelligence]<br>end<br>A --> E[DriverConsumer]<br>B --> F[EtwConsumer]<br>C --> F<br>D --> G[ThreatIntelConsumer]<br>E --> H[EventPipeline]<br>F --> H<br>G --> H<br>H --> I[ProcessModel + Enrichment]<br>I --> J[IEdrModule]<br>J --> K[AlertSink]<br>K --> L[Forwarder]<br>L --> M[Web Console]
The plan is to keep adding more telemetry sources, extending the current architecture to obtain 100% coverage of the extracted rule sets.
Every event in the engine is a BehavioralEvent carrying a typed header and a std::variant payload over 19 concrete data structs. The taxonomy covers 25 event types:
enum class EventType : std::uint16_t<br>ProcessCreate = 1, ProcessExit, ThreadCreate, ImageLoad,<br>FileCreate, FileWrite, FileDelete, FileRename,<br>RegistryCreateKey, RegistrySetValue, RegistryDeleteKey, RegistryDeleteValue,<br>NetworkConnect, DnsQuery, DriverLoad, LdapQuery, WmiOperation,<br>UserAccountCreated, ScheduledTaskCreated, ScriptExecution,<br>BehavioralIndicator, ProcessHandleAccess, EtwTiEvent,<br>NamedPipeCreate, FileSetBasicInfo,<br>};<br>The EventPipeline runs on a single worker thread, which is a deliberate choice since detection rules care about ordering: a process creation followed by a file write followed by an image load is a different story than those events arriving shuffled. A single thread guarantees that the vendor module sees events in the same order it would in a real EDR agent. Events arrive via lock-free submit() and are drained in batches to avoid per-event lock contention:
void worker_loop(std::stop_token token) noexcept<br>while (m_running.load(std::memory_order_acquire))<br>std::dequeBehavioralEvent> batch;<br>std::unique_lock lock(m_queue_mutex);<br>m_queue_cv.wait(lock, [&] {<br>return !m_queue.empty()<br>|| !m_running.load(std::memory_order_acquire);<br>});<br>batch.swap(m_queue);<br>for (auto& evt : batch)<br>process_event(evt);<br>Each process_event call follows a strict three-stage pipeline: first update the ProcessModel so all downstream consumers see current process state, then run the ModifierEngine to attach aggregate signals, then feed the event to the active vendor module. The TelemetryCorrelator may also synthesize additional events (scheduled task creation, user account creation) from raw telemetry, and those synthetic events loop back through...