Building a local-first Windows defense monitor: ETW, the Event Log, and MITRE ATT&CK, with zero telemetry

By Harasta Rahman Tri Putra (xTheoPutra) ·

Building a local-first Windows defense monitor: ETW, the Event Log, and MITRE ATT&CK, with zero telemetry

What I learned building Silent Tears, a Windows security monitor that keeps all its data on the machine, explains every alert and asks before it acts.


On one ordinary day of use, my own PC started 31,880 new processes. 28,831 of them had already exited by the time anything got around to inspecting them.

Until version 0.7.0 of Silent Tears, my Windows security monitor, those processes never existed. It compared snapshots taken a few seconds apart, so anything that started and exited in between was invisible. I tested this on purpose. A powershell.exe -EncodedCommand that ran for one second left no record. The same command kept alive for 40 seconds was caught as CRIT, 90/100, with its base64 decoded.

That gap shaped most of what I now believe about defensive tools.

The problem

I wanted a monitor for one Windows PC that would tell me who signed in, what was spawned, what connected out and what changed, and why each mattered, without sending my process names, IP addresses or file paths to anyone's server.

Windows already records much of this: sign-ins in the Security log, process creation through WMI, PowerShell and Defender in their own event channels. What's missing is something local that connects these signals, explains them in a shared vocabulary, and is honest about what it cannot see.

Three design principles

1. Local-first means zero packets

"No telemetry" turned out to be harder than not adding analytics. Up to 0.7.0, the app resolved hostnames for remote addresses through three paths, and one (nbtstat -A <ip>) sent UDP/137 straight to the target, including internet hosts my browser had just contacted. A defensive tool that probes foreign hosts behaves like a scanner. It was noisy too: 37.03% of 121,874 production log lines were the app watching its own nbtstat.exe and conhost.exe.

Now names come only from the Windows DNS client cache, which sends nothing. The cost is written down: only 23% of connected addresses got a name at measurement time. The rest read "never seen in this machine's DNS cache", which beats a made-up name or a probe packet. GeoIP is bundled and updated offline. The only feature that reaches the internet is an optional Telegram notifier, off by default and configured by you.

2. Explain every alert

Findings map to MITRE ATT&CK IDs from an offline catalogue of 150 techniques, so a warning reads as T1110.001 and not just "suspicious login". Where there's no honest mapping, it shows none rather than a guess. Correlated cases list their reasons one factor per line, and confidence is clamped to 10–95%, never 100. A result of "unknown" is never treated as safe. If an Authenticode signature can't be checked, the app says it "cannot be established" and does not call the file unsigned. The app is also never allowed to tell you your PC is safe. The strongest thing it may say is close to "of the things I know how to check, nothing is red."

3. The user confirms every action

The built-in analyst, AEGIS, reads the machine through a gateway of 25 read-only tools. Its 7 action tools (terminate, block, quarantine, isolate and so on) are registered without a function, so the gateway refuses them for every caller. An action starts as a proposal on a confirmation card. The approval token is single-use and expires after 120 seconds, and when you confirm, the target is fingerprinted again, so a recycled PID or a changed sha256 means nothing runs.

There is one exception. An auto-response mode can block or terminate on its own, but it is off by default, and its guards can't be disabled: it never touches private addresses and never acts on outbound connections.

How detection works

One QThread engine runs 20 detector classes in a poll loop. Some are off by default, and some need Administrator rights and switch themselves off cleanly without them. Each detector emits the same small ThreatEvent with a source, a severity (INFO/WARN/CRIT), a title, a detail and a meta dict.

Process births. The fix for the snapshot gap was to subscribe to process starts:

MODE_ETW = "etw"            # Win32_ProcessStartTrace (butuh admin)
MODE_SURVEI = "survei"      # __InstanceCreationEvent WITHIN 1 (tanpa admin)
...
_KUERI_ETW = "SELECT * FROM Win32_ProcessStartTrace"
_KUERI_SURVEI = ("SELECT * FROM __InstanceCreationEvent WITHIN 1 "
                 "WHERE TargetInstance ISA 'Win32_Process'")

(The code comments are in Indonesian. butuh admin means "needs admin" and tanpa admin means "without admin".)

With Administrator rights, Win32_ProcessStartTrace is driven by kernel process-start events (ETW), and those 28,831 short-lived processes became visible. Without admin, the app falls back to a WMI survey that narrows the blind window but does not close it, and it says which mode is running. Calling both "real-time" would tell a standard user the gap was closed when it had only shrunk.

The Event Log. The Security log provides failed sign-ins, lockouts and RDP activity:

WATCHED = {
    4625: ("WARN", "Login GAGAL"),
    4740: ("CRIT", "Akun terkunci (lockout)"),
    4624: ("INFO", "Login berhasil"),
    4778: ("WARN", "Sesi RDP tersambung kembali"),
}
# Logon type 10 = RemoteInteractive (RDP), 7 = unlock
RDP_LOGON_TYPE = "10"

Two bugs taught me things here. I had read fields with one layout for every event ID, so the feed showed the domain as the locked-out account; fields are now mapped per event ID from the provider's message templates. And with logon auditing off, Windows writes no 4624/4625 at all, so an empty log looks just like a quiet one. The detector now reads the audit policy at start and warns "data source off".

Channels readable without admin add PowerShell script blocks (event 4104, already decoded by PowerShell), Defender's Operational channel and, once enabled, per-process DNS queries from DNS-Client/Operational.

Process and network context. psutil connections are kept separate as inbound and outbound. A lineage module checks 12 suspicious parent→child rules (Office → PowerShell and similar) and decodes -EncodedCommand. Authenticode goes through WinVerifyTrust, including the catalogue path much of System32 relies on. Beaconing is scored with median and MAD and reported only as a "beacon-like pattern", because legitimate traffic is regular too.

ATT&CK mapping and correlation. Detectors attach technique IDs in meta. A correlation engine with 12 attack-chain rules joins events into cases: failed logons, then a successful remote logon, then a new admin account becomes one account-takeover story. A case's techniques merge the rule's IDs with the detectors' IDs, each validated against the offline catalogue (trimmed for length):

def _teknik(rule_obj: Rule, bukti: list) -> list:
    """Teknik ATT&CK aturan + yang sudah dititipkan detektor di meta."""
    out: list = []
    for tid in rule_obj.techniques:
        t = _teks(tid)
        if t and t not in out:
            out.append(t)
    for e in bukti:
        raw = e.meta.get("techniques")
        if not isinstance(raw, list):
            continue
        for tid in raw[:12]:
            t = _teks(tid)
            if not t or t in out:
                continue
            # ... each ID is validated against the offline catalogue ...
            out.append(t)
    return out[:20]

Testing approach

The suite has 1,884 tests in stdlib unittest that never touch the user's real logs or config, run by GitHub Actions on windows-latest on every push. The house rules came from my own mistakes:

My notes list seven defects a green suite missed. Tests protect what you already understand; they don't find what nobody has looked at.

What I'd do differently, and what's next

I would design around event subscription from day one rather than snapshots. I would also treat "the data source is silent" as a first-class state from the beginning, because almost every serious bug I found was some form of silence that looked like safety.

Next on the list:

Try it

Only run it on machines you own or are allowed to monitor. If you work on Windows detection engineering, I'd like to hear which signal you would add next.

Harasta Rahman Tri Putra (xTheo), founder of xTheo Labs