TH2.12 Time Window Correlation Patterns

4-5 hours · Module 2 · Free
Operational Objective
An AiTM phishing email delivered at 09:45 followed by a sign-in from a new IP at 10:23 and an inbox rule creation at 10:31 is an attack chain — but only if you can prove the temporal proximity. Time window correlation joins events from different tables within a defined time window, establishing that events are related by timing rather than just by entity. This is the pattern that turns three separate events into one attack narrative.
Deliverable: Production-ready KQL patterns for temporal proximity joins — correlating events across tables within specified time windows.
⏱ Estimated completion: 25 minutes

Timing is the correlation

Two events share the same user. That proves they happened to the same person. Two events that share the same user and occur within 30 minutes of each other prove they are likely part of the same session. Two events that share the same user, occur within 30 minutes, and represent logically sequential steps in an attack chain (phishing → sign-in → persistence) prove the attack chain.

Time window correlation is the KQL pattern that establishes temporal proximity.

Pattern 1: Event A then Event B within N hours

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Phishing email delivered  risky sign-in within 48 hours
let maxGap = 48h;
let phishEmails = EmailEvents
| where TimeGenerated > ago(7d)
| where DeliveryAction == "Delivered"
| where ThreatTypes has "Phish"
| project PhishTime = TimeGenerated,
    User = RecipientEmailAddress,
    Subject, SenderFromAddress;
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| join kind=inner phishEmails
    on $left.UserPrincipalName == $right.User
| where TimeGenerated between (
    PhishTime .. (PhishTime + maxGap))
// Sign-in occurred AFTER the phishing email, within the gap window
// This temporal ordering is critical: reverse order (sign-in before phish)
//   is coincidence, not causation
| project PhishTime, Subject, SenderFromAddress,
    SignInTime = TimeGenerated, IPAddress,
    Country = tostring(LocationDetails.countryOrRegion),
    UserPrincipalName
| sort by PhishTime asc

The between clause enforces temporal ordering: the sign-in must occur after the phishing email and within the specified window. Without temporal ordering, the join would also match sign-ins that happened before the email — which are not causally related.

Pattern 2: Event sequence within a session window

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Sign-in  MFA registration  inbox rule  all within 4 hours
// The AiTM post-compromise sequence
let sessionWindow = 4h;
let riskySignIns = SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| project SignInTime = TimeGenerated,
    UserPrincipalName, IPAddress;
let mfaRegistrations = AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName has "registered security info"
| project MFATime = TimeGenerated,
    MFAUser = tostring(InitiatedBy.user.userPrincipalName);
let inboxRules = CloudAppEvents
| where TimeGenerated > ago(7d)
| where ActionType == "New-InboxRule"
| project RuleTime = TimeGenerated, RuleUser = AccountId;
// Three-way temporal join
riskySignIns
| join kind=inner mfaRegistrations
    on $left.UserPrincipalName == $right.MFAUser
| where MFATime between (SignInTime .. (SignInTime + sessionWindow))
| join kind=inner inboxRules
    on $left.UserPrincipalName == $right.RuleUser
| where RuleTime between (SignInTime .. (SignInTime + sessionWindow))
// All three events within the session window = high confidence AiTM
| project UserPrincipalName, SignInTime, IPAddress,
    MFATime, RuleTime
// This is the TH4 core detection query  temporal correlation
//   across three data sources proving a post-compromise sequence

Pattern 3: Temporal proximity without known ordering

Sometimes you do not know which event comes first. Two suspicious events near each other in time are correlated regardless of order:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Two users signing in from the same unusual IP within 1 hour
// Shared attacker infrastructure  regardless of which user first
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| project T1 = TimeGenerated, User1 = UserPrincipalName,
    IP1 = IPAddress
| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(7d)
    | where ResultType == 0
    | project T2 = TimeGenerated, User2 = UserPrincipalName,
        IP2 = IPAddress
) on $left.IP1 == $right.IP2
| where User1 != User2
| where abs(datetime_diff('minute', T1, T2)) < 60
// Two different users, same IP, within 1 hour
// Shared attacker infrastructure or shared VPN egress
// Enrich: is this IP a known corporate egress? If not, both users
//   may be compromised via the same attacker
| project T1, User1, T2, User2, SharedIP = IP1
| distinct User1, User2, SharedIP
THREE TIME CORRELATION PATTERNSSEQUENTIAL (A → B)Event B occurs after Event Awithin window. Order matters.Phish → sign-in, sign-in → persistenceCHAIN (A → B → C)Multiple events in sequencewithin session window.Sign-in → MFA reg → inbox rulePROXIMITY (A ~ B)Two events near each other.Order does not matter.Two users, same IP, same hourSequential proves causation. Chain proves an attack path. Proximity proves shared infrastructure.

Figure TH2.12 — Three temporal correlation patterns. Each establishes a different type of relationship between events.

Try it yourself

Exercise: Run the phishing → sign-in correlation

Run Pattern 1 against your environment. How many users had a phishing email delivered followed by a risky sign-in within 48 hours? Even zero results is informative — it means no confirmed AiTM chain in the last 7 days. If results appear, they are high-priority investigation targets.

⚠ Compliance Myth: "Temporal correlation proves causation — if two events are close in time, one caused the other"

The myth: If a phishing email and a risky sign-in happen within 48 hours, the phishing email caused the compromise.

The reality: Temporal proximity is a strong indicator, not proof. The phishing email may be unrelated to the sign-in — the user may have been compromised through a different vector (credential stuffing, access broker, another phishing email not detected as malicious). Temporal correlation elevates the result from an indicator to a high-priority lead. Confirmation requires the additional enrichment dimensions from TH1.4 — particularly the behavioral dimension (what did the session do after the sign-in?) and the geographic dimension (does the sign-in IP match the phishing infrastructure?).

Extend this approach

The `between` clause in KQL enforces a time window. For strict ordering (A before B), use `B.Time between (A.Time .. (A.Time + window))`. For proximity without ordering, use `abs(datetime_diff('minute', A.Time, B.Time)) < threshold`. Campaign modules specify which pattern is appropriate for each technique — AiTM requires ordering (phish before sign-in), shared infrastructure does not.


References Used in This Subsection

You're reading the free modules of this course

The full course continues with advanced topics, production detection rules, worked investigation scenarios, and deployable artifacts. Premium subscribers get access to all courses.

View Pricing See Full Syllabus