Cheat sheet — Parsing Windows Telemetry at Scale¶
Companion to Module 03 — Parsing Windows Telemetry at Scale · CC BY 4.0 — print it, pin it, share it.
Last reviewed: 2026-07
Get-WinEvent -FilterHashtable — filter at the source (Windows)¶
# The RIGHT way: the predicate runs in the Event Log service; only matches cross into PowerShell.
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PowerShell/Operational' # or Path = 'host.evtx'
Id = 4104 # Id = 4104,4103 for a set
Level = 3 # 1=Crit 2=Err 3=Warn 4=Info 5=Verbose
StartTime = (Get-Date).AddHours(-1) # server-side time window
}
# From a saved log file (no live channel needed):
Get-WinEvent -FilterHashtable @{ Path = 'host.evtx'; Id = 4688 }
# Match an EventData field (e.g. a specific Sysmon Image) with the 'Data' key:
Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Sysmon/Operational'; Id = 1; Data = 'powershell.exe' }
Accepted keys: LogName, Path, ProviderName, Id, Level, StartTime, EndTime, Keywords,
UserID, Data. Multiple keys are AND-ed; array values within a key are OR-ed.
The anti-pattern — | Where-Object post-filter (don't)¶
# SLOW: reads EVERY event, builds a full record for each, then discards 99%.
Get-WinEvent -Path host.evtx | Where-Object { $_.Id -eq 4104 -and $_.Message -match '-enc' }
# Prove the gap on a real .evtx:
Measure-Command { Get-WinEvent -FilterHashtable @{ Path='host.evtx'; Id=4104 } } # fast
Measure-Command { Get-WinEvent -Path host.evtx | Where-Object { $_.Id -eq 4104 } } # slow
Same result on a demo file; seconds-vs-minutes on a production log. Reach for the hash table first.
XPath (-FilterXPath) — when the hash table can't express it¶
# Event ID:
Get-WinEvent -Path host.evtx -FilterXPath "*[System[EventID=4104]]"
# ID + level + a time window (last 1h, in ms):
Get-WinEvent -LogName Security -FilterXPath "*[System[EventID=4688 and TimeCreated[timediff(@SystemTime)<=3600000]]]"
# Match an EventData field by name (Sysmon Image contains powershell):
Get-WinEvent -LogName 'Microsoft-Windows-Sysmon/Operational' `
-FilterXPath "*[EventData[Data[@Name='Image'] and (Data='C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe')]]"
-FilterXPath (per-query) and -FilterXml (a full <QueryList>) also run server-side. Use XPath for
EventData-field predicates and OR across providers the hash table can't do.
Cross-platform: .evtx -> JSON (Linux / the lab container)¶
# Get-WinEvent is Windows-only. On Linux, parse .evtx with the 'evtx' Rust CLI:
cargo install evtx # installs evtx_dump
evtx_dump -o jsonl sample.evtx > events.jsonl # one JSON event per line (JSONL)
evtx_dump -o json sample.evtx > events.json # single JSON array
Each JSONL line is one record: { "Event": { "System": {...}, "EventData": {...} } }.
# Read the exported artifact with the SAME source-side discipline: filter as you stream.
Get-Content ./events.jsonl | ForEach-Object {
$e = ($_ | ConvertFrom-Json).Event
if ([int]$e.System.EventID -ne 4104) { return } # skip BEFORE building an object
[pscustomobject]@{
TimeCreated = [datetime]$e.System.TimeCreated.'#attributes'.SystemTime
EventId = [int]$e.System.EventID
Provider = [string]$e.System.Provider.'#attributes'.Name
CommandLine = [string]($e.EventData.CommandLine, $e.EventData.ScriptBlockText -ne $null)[0]
}
}
Get-VigilEvent -Path ./data/events.jsonl -EventId 4104 -Provider '*PowerShell*' is this, packaged.
The events you hunt (fields to normalize)¶
Security 4688 process creation -> NewProcessName + CommandLine (CommandLine off by default)
Security 4624 logon -> TargetUserName + LogonType + IpAddress
Sysmon 1 process creation -> Image + CommandLine + ParentImage + User
Sysmon 3 network connection -> Image + DestinationIp + DestinationPort
PowerShell 4104 script block -> ScriptBlockText (the encoded/IEX payload lives here)
Normalize once at the edge into one flat object so downstream code never re-parses per-provider fields.
Structured JSON logs — ConvertTo-Json, not Write-Host¶
# One compact, shippable JSON line per record: capturable, greppable, SIEM-ingestible.
[pscustomobject]@{
timestamp = (Get-Date).ToUniversalTime().ToString('o')
level = 'Warning'
message = 'suspicious 4104'
EventId = 4104
Host = 'WKS07'
} | ConvertTo-Json -Compress -Depth 5
# vs. the anti-pattern (returns nothing; can't be piped, captured, or shipped):
Write-Host "suspicious 4104 on WKS07" # <- PSAvoidUsingWriteHost; dies on the console
Write-VigilLog -Message 'hunt started' -Level Info -Data @{ Source = 'events.jsonl' }
Write-VigilLog -Message 'noisy' -Level Debug -MinimumLevel Info # suppressed (below threshold)
$log | ConvertFrom-Json | Where-Object level -eq 'Warning' # your logs are now queryable
The real corpus (get more telemetry)¶
EVTX-ATTACK-SAMPLES real .evtx per ATT&CK tactic (Execution, Defense Evasion, Credential Access...)
github.com/sbousseaden/EVTX-ATTACK-SAMPLES -> evtx_dump -o jsonl to feed Get-VigilEvent
SwiftOnSecurity sysmon-config annotated baseline: which events to log and why
github.com/SwiftOnSecurity/sysmon-config -> shapes what good Sysmon telemetry contains
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).