What Is Event Correlation in SIEM?
HuntRule Team · · 10 min read

On this page
- The four types, precisely
- Counting over a window (event_count)
- Unique-value counting (value_count)
- Temporal sequence (temporal)
- Temporal ordered sequence (temporal_ordered)
- The join key decides everything
- Clock skew and late arrival
- Window length is a base-rate decision
- A worked rule
- Backend support is uneven and correlation is not free
- When to correlate
- ATT&CK techniques referenced
- Summary
Windows Security event 4625 on its own is a typo. Thirty of them against thirty different account names from one source address in fifteen minutes, followed by a 4624 from that same address, is a password spray that worked. Nothing in the first event carries that conclusion. The conclusion lives in the relationship between events.
That relationship is what event correlation means in a SIEM. Every correlation is three things: a set of base events, a key those events share, and a time window. The join is where it works and the join is where it fails.
The four types, precisely
Sigma expresses correlation in a correlation section instead of a detection section, in a rule that references other rules by id or by name. Spec version 2.1.0, dated 2025-08-02, is inconsistent with itself about how many types exist. Its history section records value_sum, value_avg and value_percentile as added in that release, and the body gives each of the three its own section and example, but the normative list of allowed values for type still names only event_count, value_count, temporal and temporal_ordered. Those four are the ones you will actually write, and they are the four below.
Counting over a window (event_count)
Counts events matching the referenced rule inside timespan, per group-by key. The condition is a map of criteria drawn from gt, gte, lt, lte, eq and neq.
title: Many failed logins
id: 0e95725d-7320-415d-80f7-004da920fc11
correlation:
type: event_count
rules:
- 5638f7c0-ac70-491d-8465-2a65075e0d86
group-by:
- ComputerName
timespan: 1h
condition:
gte: 100A hundred 4625s against one host in an hour. Note the key. Group by ComputerName and you find hosts under attack, group by TargetUserName and you find accounts under attack. Same events, different detection.
Unique-value counting (value_count)
Counts distinct values of one field, named in condition.field, per group key. This is the type that separates a spray from a guess.
title: Failed login
id: 0e95725d-7320-415d-80f7-004da920fc12
correlation:
type: value_count
rules:
- 5638f7c0-ac70-491d-8465-2a65075e0d86
group-by:
- ComputerName
- WorkstationName
timespan: 1d
condition:
field: User
gte: 100One source, a hundred distinct usernames, one day. An event_count of a hundred failures also fires on a service account with a stale password retrying in a loop. The distinct count does not.
Temporal sequence (temporal)
All referenced rules must match inside timespan, in any order, with the same values for every group-by field. There is no condition. Recon behaviour is the standard case, because each command on its own is administrative.
correlation:
type: temporal
rules:
- recon_cmd_a
- recon_cmd_b
- recon_cmd_c
group-by:
- ComputerName
- User
timespan: 5mIn practice those three are Sysmon Event ID 1 rules for whoami.exe (T1033), net.exe with group "Domain Admins" /domain (T1087.002) and nltest.exe with /dclist: (T1018). Any one of them is a Tuesday. All three by the same account on the same host inside five minutes is not.
Temporal ordered sequence (temporal_ordered)
Identical to temporal, plus the events must appear in the order the rules are listed under rules.
correlation:
type: temporal_ordered
rules:
- many_failed_logins
- successful_login
group-by:
- User
timespan: 1hOrder is the whole point. Failures then a success is a brute force that landed. A success then failures is somebody who logged in and later fat-fingered a runas. Note also what the spec says about that inner rule: even if many_failed_logins groups by ComputerName, the correlation uses only its own group-by, which is User.
The join key decides everything
A correlation is only as good as the identifier the events genuinely share. Ranked by how much abuse they survive:
ProcessGuid. Sysmon stamps a globally unique GUID on Event ID 1, and the same GUID appears on Event ID 3 network connections and Event ID 11 file creates from that process. It is not reused. Joining on PID instead will eventually join two unrelated processes, because Windows recycles PIDs.Logon session. Event 4624 emits
TargetLogonId, and 4688 emitsSubjectLogonIdfor processes started in that session. Exact, but host-local.LogonIdis unique per host per boot, so it must be paired with the hostname.Account name. Survives crossing log sources, but
CORP\jsmith,jsmith@corp.localandjsmithare three different strings to agroup-by.Source address. NAT collapses a whole office behind one value, and a VPN concentrator will happily correlate two unrelated users.
When the same real-world value has different field names on either side, Sigma has aliases, which map an alias to a source field per referenced rule name for use in group-by:
correlation:
type: temporal
rules:
- internal_error
- new_network_connection
group-by:
- internal_ip
- remote_ip
timespan: 10s
aliases:
internal_ip:
internal_error: destination.ip
new_network_connection: source.ip
remote_ip:
internal_error: source.ip
new_network_connection: destination.ipThe name attribute is optional on a normal Sigma rule. It is mandatory on any rule referenced from aliases.
Clock skew and late arrival
Every temporal correlation orders events by a timestamp, and which timestamp is a decision most teams never make deliberately. If ordering runs on the event's own generated time, a domain controller two seconds ahead of a member server can put the successful logon before the failures and silently break temporal_ordered. If it runs on ingest time, agent batching decides the order instead.
Late arrival is the same problem from the other end. Shippers buffer and retry. An endpoint that was asleep sends its 4624 with a correct original timestamp long after the search covering that period has run, so a correlation running every five minutes over the last five minutes never sees it. The usual fix is a lookbehind delay plus an overlapping window, at the cost of duplicate alerts you then suppress.
Window length is a base-rate decision
Too short and slow attacks walk through. A spray at one attempt per account per hour never reaches fifteen distinct accounts in a fifteen-minute window. Too long and you are not detecting a relationship any more, you are detecting coincidence. Failed and successful logons for the same user are both common, and over a 24-hour window on a busy source address they co-occur constantly with no attack involved. Pick the window from the tempo of the behaviour, then measure how many groups the query returns on a quiet week before you attach an alert to it.
A worked rule
Password spray from one source, followed by a successful interactive or network logon from that same source, within thirty minutes.
title: Password spray from a single source followed by a successful logon
id: 4a0f9c1e-2b7d-4d21-9c3e-8f1a6b2d4e70
status: experimental
description: Failed logons against many distinct accounts from one source address, followed by a successful logon from that address
author: HuntRule
date: 2026-07-31
correlation:
type: temporal_ordered
rules:
- spray_many_accounts
- successful_logon
group-by:
- IpAddress
timespan: 30m
falsepositives:
- Vulnerability scanners and pentest tooling authenticating from a fixed address
- A shared NAT egress address where an expired password storm coincides with normal logons
level: high
---
title: Failed logons against many distinct accounts from one source
id: 9d2c7b41-5f88-4a0e-b6c3-1e7d5a904f22
name: spray_many_accounts
correlation:
type: value_count
rules:
- failed_logon
group-by:
- IpAddress
timespan: 15m
condition:
field: TargetUserName
gte: 15
---
title: Failed logon
id: c1f6a83b-7e40-4b95-8d2a-0c5b9e3f6714
name: failed_logon
logsource:
product: windows
service: security
detection:
selection:
EventID: 4625
filter_local:
IpAddress:
- '-'
- '127.0.0.1'
- '::1'
condition: selection and not filter_local
---
title: Successful logon
id: 7b3e5d92-4c16-42af-9e80-2a6f8c1b5d33
name: successful_logon
logsource:
product: windows
service: security
detection:
selection:
EventID: 4624
LogonType:
- 3
- 10
condition: selectionThe exact assumptions it makes, all worth checking against your own data first:
IpAddressis populated and formatted identically in 4625 and 4624. Local and cached logons write-or a loopback address, which is why the base rule filters them. Correlating on those values would join the entire estate into one group.The windows are nested. Fifteen distinct accounts must fail inside fifteen minutes, and the successful logon must land inside a thirty-minute window that starts with the spray. A spray that succeeds forty minutes later does not fire.
Ordering is by whatever timestamp the backend sorts on. The 4625s usually come from a domain controller and the 4624 may come from a different host, so the two clocks must agree to well inside thirty minutes.
Both streams sit in one searchable index with a comparable time field. Different retention on the two sides produces silent false negatives.
Fifteen distinct accounts is a guess until you have run the
value_counthalf alone against your own history.
Known blind spot: Kerberos. A spray against a domain controller over Kerberos produces event 4771, Kerberos pre-authentication failed, with failure code 0x18 for a bad password. It does not produce 4625. Covering it means a second base rule on 4771, whose IpAddress a DC often writes in mapped form such as ::ffff:10.0.0.5. That formatting difference is the kind of thing that quietly kills a join.
Backend support is uneven and correlation is not free
Sigma correlations are newer than Sigma detections and backend coverage reflects that. The SigmaHQ documentation currently lists support in Splunk SPL, Elasticsearch ES|QL, Grafana Loki, SQL and OpenSearch. The specification requires a backend to raise an error when a mandatory feature is unsupported, and to warn on softer gaps. It names two worth knowing. A backend that recognises temporal proximity but not ordering produces false positives from differently ordered events. A backend that only evaluates within fixed boundaries, where a one-hour timespan means the clock hour rather than any rolling hour, produces false negatives when events straddle the boundary. Convert the rule and read the warnings before trusting it.
The runtime cost is real too. A stateless detection evaluates one event and forgets it. A correlation holds state for every distinct group key seen in the window, and temporal types keep partial matches alive waiting for the rest of the sequence. Memory and CPU scale with the cardinality of your group-by, which is why IpAddress behaves very differently from ComputerName on the same estate. It is also why correlation runs on a schedule over a lookback window rather than in a streaming path.
When to correlate
Correlate when a single event genuinely cannot carry the decision, not because correlation sounds like the more advanced technique. If one 4688 command line is enough to page someone, write a plain Sigma rule and move on. Correlation earns its cost in two cases: the individual events are too common to alert on alone, or the ordering or count is the malicious part. Everything else is a stateless match wearing a costume, and that decision is covered further in what a detection rule is.
ATT&CK techniques referenced
Technique | ID | Where it appears |
|---|---|---|
System Owner/User Discovery | T1033 |
|
Remote System Discovery | T1018 |
|
Account Discovery: Domain Account | T1087.002 |
|
Brute Force: Password Spraying | T1110.003 | The worked rule |
Valid Accounts | T1078 | The successful 4624 |
Summary
Correlation is a join with a clock on it. Sigma gives four practical types: event_count for volume, value_count for distinct values, temporal for co-occurrence and temporal_ordered for sequence. The rule holds together only if the group-by key really identifies the same entity on both sides, if the clocks agree to well inside the window, and if late events arrive before the search runs. Backend support is partial and stateful correlation costs more than a stateless match, so spend it where a single event cannot carry the decision and nowhere else. Base rules to correlate against are in the Windows rules section of the catalog.
Collect these before writing any of the above:
4624 An account was successfully logged on (TargetUserName, TargetLogonId, LogonType, IpAddress)
4625 An account failed to log on (TargetUserName, LogonType, Status, SubStatus, IpAddress)
4634 An account was logged off (TargetUserName, TargetLogonId, LogonType)
4688 A new process has been created (NewProcessName, CommandLine, SubjectLogonId)
4771 Kerberos pre-authentication failed (TargetUserName, Status, IpAddress)
Sysmon 1 Process creation (Image, CommandLine, ProcessGuid, ParentProcessGuid, User)
Sysmon 3 Network connection detected (Image, ProcessGuid, DestinationIp, DestinationPort)
Sysmon 11 File created (Image, ProcessGuid, TargetFilename)Related articles

What Is Log Normalization in SIEMs?
Sysmon calls it Image. Windows Security event 4688 calls it NewProcessName. Microsoft Defender for Endpoint splits the same thing into FolderPath and FileName. All three describe one executable…
2026-02-268 min read

What Is SOAR?
POST /devices/entities/devices-actions/v2?action_name=contain takes a CrowdStrike host off the network. action_name=lift_containment puts it back. Both are one HTTP call against the same endpoint,…
2025-08-1810 min read

What Is UEBA?
Microsoft Sentinel's BehaviorAnalytics table stores an integer called InvestigationPriority, scored 0 to 10, where 0 is benign and 10 is highly anomalous. That integer is the whole promise and the…
2025-08-069 min read