Taming the Windows noise floor: rule 60107 and lab rule 100230 (2026-10-30)
Owner: detection-engineer. Closes the analysis half of the P0 roadmap task "Tame Windows baseline noise: rule 60107 (Security 4673) + lab rule 100230". The manager deploy and a measured post-deploy baseline are still open (operator step, see the end).
Reproduce (lab host only, raw alerts.json is gitignored): python -X utf8 analysis/noise_tuning.py [--prefix 2026-10-2] [--json]. The replay reads the tuning predicates straight out of local_rules.xml, so the numbers below are what the shipped rules do, not a hand-copied approximation. analysis/tests/test_noise_tuning.py pins the same rules in CI.
What the raw events say
The 2026-10-27 report could only see rule ids, because the committed run.json summaries carry no event fields. The raw window alerts on the lab host do. Across 182 local bundles (163 with Windows window alerts):
Rule 60107 (158,534 alerts). One tuple is 99.7% of it (158,126):
| field | value | share |
|---|---|---|
| process | C:\Windows\System32\svchost.exe | 99.7% |
| privilege | SeTcbPrivilege (alone) | 100% |
| account | built-in Administrator (RID 500) | 100% |
| result | AUDIT_FAILURE | 100% |
The remaining 0.3% is explorer.exe (205), powershell.exe (145), lsass.exe as the machine account (46), and a handful of vmtoolsd/SearchApp/StartMenu hits.
Why it happens. Every burst of about 195 of these events sits inside one VMware Tools guest-operation logon: a 4624 type 4 (batch) logon of Administrator by C:\Program Files\VMware\VMware Tools\vmtoolsd.exe, logon process Advapi, closed by a 4634 about 3 seconds later. The subjectLogonId on the 4673s is that logon's id. These are the harness's own vmrun runProgramInGuest calls. Short guest-ops (about 15 ms, file and process polls) produce none; the ~3 s program runs produce ~195 each. The svchost PID is the same in every run because it is captured in the golden snapshot.
So most of the Windows "baseline noise" was the measurement instrument, not the host: golden v2 turned on Sensitive Privilege Use success+failure auditing, and every guest-op logon our harness makes now trips ~195 failed SeTcbPrivilege checks in a per-session svchost.
Rule 100230 (554 alerts, L10). Every one had a service-account parentUser: SYSTEM (517), LOCAL SERVICE (23), SYSTEM parent of a NETWORK SERVICE child (12). 72% are conhost.exe spawned under SYSTEM console apps, mostly the 32-bit Wazuh agent's SCA checks (SysWOW64 auditpol, powershell, SecEdit /export). The rest is MpCmdRun, EdgeUpdate, ngen, CompatTelRunner.
Neither rule was ever a matched or expected rule in any run bundle, so no past grade depends on them.
The tuning
New rule 100450, level 0, child of 60107. Matches only: Security 4673, AUDIT_FAILURE, process exactly C:\Windows\System32\svchost.exe, privilege list exactly SeTcbPrivilege, subject SID ending -500. Level 0 means no alert; the event still reaches the archives if logall is on. No <mitre> tag, so it is never predicted or graded as a detection.
Rule 100230 gains one condition: parentUser must not be NT AUTHORITY\SYSTEM, LOCAL SERVICE or NETWORK SERVICE. A SYSTEM parent creating a SYSTEM child is not token manipulation, so this is the test the rule was missing, not a blanket exclusion.
What still alerts (deliberately)
| abuse shape | still fires? | why |
|---|---|---|
| SeDebugPrivilege failure (unprivileged lsass-dump attempt) | yes, 60107 | privilege list must be exactly SeTcbPrivilege |
| a 4673 listing SeTcb plus another privilege | yes, 60107 | exact match on the list |
| any non-svchost process failing SeTcb (implant, mimikatz, powershell) | yes, 60107 | exact image path |
svchost.exe running outside System32 (masquerade) | yes, 60107 | path is anchored |
| any other account | yes, 60107 | RID-500 anchor |
| success-side 4673/4674 | untouched | 60107 is the failure rule |
| user-context parent creates a SYSTEM-token child (token theft, CreateProcessWithToken) | yes, 100230 | parentUser is the user, not a service account |
Known trade-offs, stated rather than hidden:
- A real abuse that happens to be svchost failing SeTcb as the built-in Administrator would be silenced. Process hollowing of a System32 svchost is the plausible case; it has much better signals (Sysmon EID 8/10, 100204-style access) than a failed privilege check.
- 100230 no longer fires when a SYSTEM-context launcher outside its
parentImagelist (for examplePSEXESVCforpsexec -s) starts a SYSTEM process. That is service execution (T1569.002), not token manipulation, and it should have its own rule if we want it. - 100230 now needs Sysmon
ParentUser(present on win-victim v3). Without the field the condition fails and the rule does not fire. A Sysmon downgrade would silently blind it.
Expected effect (replayed on the raw window alerts)
| before | after | change | |
|---|---|---|---|
| Windows window alerts, all runs (163) | 174,792 | 16,111 | -90.8% |
| median 90 s window, all runs | 1,491 | 124 | -92% |
| median window, 2026-10-2x runs (89) | 1,737 | 146 | -92% |
| rule 60107 | 158,534 | 407 | -99.7% |
| rule 100230 | 554 | 0 | -100% |
| baseline hits per 30 min (median, 89 runs, projected) | 5,000 (capped) | about 400 | about -92% |
The baseline row needs care. 27 of 29 baselines hit the 5,000-hit cap, so the "before" is a floor, and the "after" is projected by scaling each rule's capped baseline count by the share the replay removed. It is an estimate, not a measurement. What it does say is that the baseline should land an order of magnitude under the cap, which is the point: the cap stops hiding things, and the manager's ~5,000 alerts per 30 min saturation point stops being the wave-size limit. The first post-deploy wave's noise_baseline.py run is the real number.
What we did not change, and the other option
The alternative is fixing it at the source: turn failure auditing off for Sensitive Privilege Use (auditpol /set /subcategory:"Sensitive Privilege Use" /failure:disable) in enable-telemetry.ps1, or cut the number of long runProgramInGuest calls the harness makes. Both are bigger changes. The policy change needs a golden-image rebuild and would also drop the failure events we want to keep (SeDebugPrivilege failures). The harness change reduces the noise but does not remove it. The rule keeps all telemetry and is reversible with one file.
Measured after deploy (2026-10-30)
AJ deployed local_rules.xml (with the Wave 2 rules, #158) on the lab Wazuh manager. verify_rules_live reported 69 shipped, all live, and 100450 was ungated (#163). The first wave after the deploy was the 7-entry Wave 2 firing proof (run-2026-10-30-001..007, all 7 detected). Read from those bundles' run.json and raw alerts.json:
| before (2026-10-2x) | after (2026-10-30, 7 runs) | |
|---|---|---|
| baselines that hit the 5,000 cap | 44 of 50 Windows runs | 0 of 7 |
| baseline hits per 30 min | 5,000 (cap, a floor) | 474 to 1,629, median 1,138 |
| rule 60107 per baseline | ~4,000+ | 3 to 19 |
| rule 100230 per baseline | ~43 | 0 |
| median 90 s detonation window | 1,737 | 148 (range 141 to 245) |
The window figure matches the replay's prediction (146) almost exactly. The baseline came in above the ~400 projection, and it climbs run by run through the batch (474, 797, 965, ... 1,629). The growth is the 4624/4634/4672 logon family (67022/67023/67028, about +41 each per run) plus 4688 (67027). Those are the same VMware Tools guest-op logons, now visible because 60107 no longer buries them. They accumulate because each baseline window overlaps the previous entries' harness activity. 60107 itself is gone as a noise source. Every baseline is now uncapped, so "this rule never fired in the baseline" is a real measurement again.
Next noise source, if we want the floor lower: the harness's own type-4 vmtoolsd.exe logons through the 670xx logon rules. They are a smaller, separate tuning question. Each one is also the audit trail of a guest operation, so silencing them is less obviously free than 60107 was.
Follow-up: the harness logon family (100451-100453, 2026-10-31 to 2026-11-02)
PR #177 added three level-0 children: 100451 (4624 under 67022: type 4, Advapi, caller exactly vmtoolsd.exe, RID-500), 100452 (4634 under 67023: type 4, RID-500) and 100453 (4672 under 67028: RID-500 only, which is the deliberate trade-off, since 4672 carries no caller or logon type). 67027 (4688) is untouched because detection arms chain it and atomics are launched by vmtoolsd. Rationale and what still alerts are in the rule comments.
| before | after | |
|---|---|---|
| host replay, runs 2026-10-30-001..007: 67022 / 67023 / 67028 | 268 / 271 / 268 | 0 / 0 / 0 (100% removed) |
| host replay: median detonation window | 148 | 37 |
| live, first post-deploy runs (run-2026-11-02-013..016), detections | n/a | 4 of 4 DETECTED |
| live: 67022 / 67023 / 67028 per baseline | ~295 each (run-2026-10-30-007) | 0 to 2 |
| live: Windows baseline hits per 30 min | 474 to 1,629 | 116 to 264 |
Limit: every 670xx event in the replayed windows was harness-generated, so the replay proves the rules match the harness but cannot exercise the "spares a real logon" side; analysis/tests/test_noise_tuning.py pins that. The remaining floor is mostly 67027 (49 to 123 per baseline), the harness's own process launches.