Sysmon Tuning: Signal Without the Flood
Sysmon out of the box will bury you. A per-event-ID walkthrough of what to keep, what to drop, and how to work out the licence cost before you deploy to 10,000 endpoints.
Sysmon is the highest-value endpoint telemetry you can deploy for free, and the fastest way to blow a SIEM licence. Both statements are true, and the gap between them is entirely configuration.
A default sysmon -i on 10,000 endpoints produces something like 300–600 GB a day. Tuned, the same estate produces 40–80 GB with better detection coverage, because the noise that was hiding the signal is gone.
Start from a known baseline
Do not write a config from scratch. Start from one of the two maintained baselines and modify:
- SwiftOnSecurity’s
sysmonconfig-export.xml- conservative, heavily commented, a good teaching document. - Olaf Hartong’s
sysmon-modular- modular, ATT&CK-tagged, generates a merged config from per-technique fragments.
sysmon-modular is the better fit for a detection-as-code workflow: each fragment is a file, the merge is a build step, and the ATT&CK tags flow into your coverage reporting. Treat the merged XML as a build artefact, never edit it directly.
# In your telemetry repo
git submodule add https://github.com/olafhartong/sysmon-modular vendor/sysmon-modular
python vendor/sysmon-modular/Merge-SysmonXml.py \
--include-fragments config/fragments/ \
--output build/sysmonconfig.xml
The event IDs that matter
Sysmon emits 29 event types. You do not want all of them.
Event ID 1 - Process creation
Keep it. All of it. This is the backbone. More detections are built on EID 1 than every other Sysmon event combined.
Resist the urge to exclude noisy processes. Excluding svchost.exe feels like an easy win and blinds you to process hollowing and masquerading - two techniques that depend on looking like svchost.exe.
Exclude narrowly, by full path plus parent, never by image name alone:
<ProcessCreate onmatch="exclude">
<Rule groupRelation="and">
<Image condition="is">C:\Program Files\Datadog\agent.exe</Image>
<ParentImage condition="is">C:\Windows\System32\services.exe</ParentImage>
</Rule>
</ProcessCreate>
An attacker who drops agent.exe in Temp still generates an event. An attacker who achieves the exact path and the exact parent has already won elsewhere.
Event ID 3 - Network connection
The single biggest volume source. Unfiltered, this is half your Sysmon bill.
Exclude by destination, keep by process. What you actually care about is unusual processes making connections, not the volume of connections from Chrome.
<NetworkConnect onmatch="exclude">
<DestinationIp condition="is">127.0.0.1</DestinationIp>
<DestinationIp condition="begin with">10.</DestinationIp>
<Image condition="end with">\chrome.exe</Image>
<Image condition="end with">\msedge.exe</Image>
<Image condition="end with">\Teams.exe</Image>
</NetworkConnect>
Excluding RFC1918 destinations is a real trade-off: you lose internal lateral-movement visibility from this source. Accept it only if you have network telemetry covering east-west traffic. If you do not, keep internal destinations and exclude harder on the process side instead.
Event ID 7 - Image loaded
High value, brutal volume. Unfiltered this rivals EID 3. Run it in include mode only:
<ImageLoad onmatch="include">
<ImageLoaded condition="end with">\clr.dll</ImageLoaded>
<ImageLoaded condition="end with">\mscoree.dll</ImageLoaded>
<ImageLoaded condition="end with">\vaultcli.dll</ImageLoaded>
<ImageLoaded condition="end with">\samlib.dll</ImageLoaded>
<ImageLoaded condition="end with">\wldap32.dll</ImageLoaded>
<ImageLoaded condition="contains">\Temp\</ImageLoaded>
<Signed condition="is">false</Signed>
</ImageLoad>
clr.dll and mscoree.dll in an unexpected process means .NET assembly loading - Cobalt Strike’s execute-assembly, most C# tradecraft. vaultcli.dll and samlib.dll are credential-access primitives.
Event ID 8 - CreateRemoteThread
Keep, include mode, low volume. Classic injection. Exclude legitimate injectors by signed publisher and you are left with a handful of events a day across a large estate.
Event ID 10 - Process access
The most valuable and the most expensive. This is your LSASS detection. Include mode, tightly scoped:
<ProcessAccess onmatch="include">
<TargetImage condition="end with">\lsass.exe</TargetImage>
<TargetImage condition="end with">\winlogon.exe</TargetImage>
</ProcessAccess>
Then exclude the known-good accessors within that include - EDR agents, backup agents, WerFault.exe. Do it by full signed path, and re-check the list after every agent upgrade; vendors move binaries between versions and a stale exclusion is a blind spot.
Budget 5–15 events per endpoint per day after tuning. If you see hundreds, an agent is not excluded properly.
Event ID 11 - File create
Include mode, targeted paths. Everything is a file create; you cannot keep it all.
<FileCreate onmatch="include">
<TargetFilename condition="contains">\Startup\</TargetFilename>
<TargetFilename condition="contains">\Start Menu\Programs\Startup</TargetFilename>
<TargetFilename condition="end with">.lnk</TargetFilename>
<TargetFilename condition="end with">.hta</TargetFilename>
<TargetFilename condition="end with">.ps1</TargetFilename>
<TargetFilename condition="contains all">\Users\;\Downloads\;.iso</TargetFilename>
</FileCreate>
Event ID 12/13/14 - Registry
Include mode, autoruns and known abuse keys. The full registry firehose is unusable; the autorun subset is one of the best persistence detections available. sysmon-modular ships a good fragment - take it as-is.
Event ID 22 - DNS query
Keep if you can afford it. Excellent for C2 detection and often the only DNS visibility you have on roaming laptops.
Exclude aggressively by domain, and understand what you are giving up:
<DnsQuery onmatch="exclude">
<QueryName condition="end with">.microsoft.com</QueryName>
<QueryName condition="end with">.windowsupdate.com</QueryName>
<QueryName condition="end with">.office365.com</QueryName>
<QueryName condition="end with">.in-addr.arpa</QueryName>
</DnsQuery>
Every exclusion here is a domain an attacker can hide behind. .microsoft.com is a reasonable exclusion; a bare .com would be absurd. Draw the line deliberately and write down why.
Drop these
- EID 2 (file creation time changed) - timestomping detection, very low hit rate, moderate volume. Skip unless you have a specific reason.
- EID 4 (Sysmon state change) - keep, it is one event, and its absence tells you Sysmon was tampered with.
- EID 5 (process terminated) - useful for correlation, rarely worth the volume. Drop first if you need savings.
- EID 9 (raw disk access) - noisy from backup software, low yield.
- EID 15 (file stream created) - mark-of-the-web tracking, moderate value, keep if budget allows.
Work out the cost first
Do not guess. Deploy to 20 representative endpoints for a week, then measure:
index=sysmon earliest=-7d
| eval mb=len(_raw)/1024/1024
| stats sum(mb) as total_mb dc(host) as hosts by EventCode
| eval mb_per_host_per_day = total_mb / hosts / 7
| eval projected_gb_per_day = (mb_per_host_per_day * 10000) / 1024
| sort - projected_gb_per_day
Pick 20 endpoints that actually represent the estate: a few developer workstations, a few call-centre desktops, a domain controller, a file server, a build agent. Twenty developer laptops will mislead you badly in both directions - high on process creation, low on SMB.
Two numbers matter from that output: total projected GB/day, and the per-event-ID breakdown. The breakdown is your negotiating position. When someone says the licence will not stretch, you can say “EID 3 is 60% of this and I can halve it by excluding internal destinations - here is what we lose.”
Roll out in rings
- Ring 0 - 20 endpoints, one week. Measure volume, catch crashes.
- Ring 1 - IT and security team machines, two weeks. These people will tell you immediately if something breaks.
- Ring 2 - 10% of the estate, one week. Watch SIEM ingestion and indexer load, not just endpoints.
- Ring 3 - everything else.
Between each ring, re-run the volume query. Ring 2 routinely surprises people because servers behave nothing like workstations.
Version the config like code
The Sysmon config is a detection artefact. Every rule you write depends on it, which means a config change can silently disable detections.
Three practices worth the effort:
Bump schemaversion and an internal version comment on every change, and ship the config version as a field in EID 4 so you can query which endpoints are on which config.
Diff configs in review. A merged sysmonconfig.xml diff is unreadable, which is exactly why you keep the fragments as the source of truth and review those.
Test the detections that depend on the block you touched. Widening an EID 10 exclusion should re-run the LSASS rule tests. If those tests do not exist yet, that is the tell - the config is coupled to your detections and you have no way to see it.
The failure mode is quiet: someone adds a sensible-looking exclusion, three detections stop working, and nobody finds out until an incident. A config change that touches an exclusion block deserves the same review as a change to the rule itself, because operationally it is a change to the rule.