In this module
EI1.10 Cross-Table Joins and Correlation
Why cross-table correlation matters
Single-table queries answer single-dimension questions: who signed in, what authentication method was used, what conditional access decided. But real identity attacks span multiple data sources. The initial access appears in SigninLogs. The persistence mechanisms appear in AuditLogs (consent grants, role assignments, credential additions). The lateral movement appears in AADNonInteractiveUserSignInLogs (token refresh from a new IP). The impact appears in OfficeActivity (file downloads, email forwarding rules).
Without cross-table correlation, each of these events looks unremarkable on its own. A successful sign-in is normal. A consent grant is routine. A file download is expected. Only when you correlate them — this user had a risky sign-in, then consented to an unknown application, then downloaded 500 files in 30 minutes — does the attack pattern emerge.
The join operator
KQL's join operator combines rows from two tables based on a matching condition. The basic syntax is:
TableA
| join kind=inner TableB on CommonFieldFor identity security, the most common join patterns are:
// EI1.10 — Correlate risky sign-ins with subsequent admin actions
let riskyUsers = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| distinct UserPrincipalName, TimeGenerated;
AuditLogs
| where TimeGenerated > ago(24h)
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| join kind=inner riskyUsers on $left.Actor == $right.UserPrincipalName
| where TimeGenerated > TimeGenerated1 // Audit event after the risky sign-in
| project
SignInTime = TimeGenerated1,
AuditTime = TimeGenerated,
User = Actor,
Operation = OperationName,
Target = tostring(TargetResources[0].displayName),
MinutesAfterSignIn = datetime_diff('minute', TimeGenerated, TimeGenerated1)
| where MinutesAfterSignIn < 120 // Within 2 hours of the risky sign-in
| order by User, AuditTime asc
// Results: administrative actions taken by users who had risky sign-ins
// Look for: "Add member to role", "Add service principal credentials",
// "Consent to application", "Update conditional access policy"
// These are Stage 3-4 kill chain activities performed after suspicious access// EI1.10 — Detect concurrent sessions from different IPs
// Strongest indicator of token theft in progress
let timeWindow = 1h;
let interactiveSessions = SigninLogs
| where TimeGenerated > ago(timeWindow)
| where ResultType == 0
| summarize InteractiveIP = arg_max(TimeGenerated, IPAddress)
by UserPrincipalName
| project UserPrincipalName, InteractiveIP = IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(timeWindow)
| where ResultType == 0
| summarize NonInteractiveIPs = make_set(IPAddress, 20)
by UserPrincipalName
| join kind=inner interactiveSessions on UserPrincipalName
| extend HasDifferentIP = array_length(
set_difference(NonInteractiveIPs, dynamic([InteractiveIP]))) > 0
| where HasDifferentIP
| project
UserPrincipalName,
InteractiveIP,
NonInteractiveIPs,
OtherIPCount = array_length(
set_difference(NonInteractiveIPs, dynamic([InteractiveIP])))
// Results: users whose non-interactive sessions come from IPs
// different than their interactive session
// Common benign causes: VPN split tunnel, mobile network, Microsoft CDN IPs
// Suspicious: non-interactive IP in different country than interactive IP// EI1.10 — Complete sign-in view: interactive + non-interactive
union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(24h)
| where UserPrincipalName == "casey.finance@yourdomain.onmicrosoft.com"
| where ResultType == 0
| project
TimeGenerated,
SignInType = iff(Type == "SigninLogs", "Interactive", "Non-Interactive"),
AppDisplayName,
IPAddress,
Country = tostring(LocationDetails.countryOrRegion),
ConditionalAccessStatus
| order by TimeGenerated asc
// Complete chronological view of a user's authentication activity
// Useful for incident timelines and baseline comparisonCorrelating with audit logs
The AuditLogs table records every administrative and configuration change in Entra ID. The most security-critical operations to correlate with sign-in activity are:
Consent grants: OperationName == "Consent to application" — a user or admin granted permissions to an application. Correlate with the sign-in that preceded it: was the user's sign-in risky?
// EI1.10 — High-risk audit events correlated with sign-in context
let highRiskOperations = dynamic([
"Consent to application",
"Add member to role",
"Add service principal credentials",
"Update conditional access policy",
"Add application",
"Update application – Certificates and secrets management"
]);
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in (highRiskOperations)
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend Target = tostring(TargetResources[0].displayName)
| extend TargetType = tostring(TargetResources[0].type)
| lookup kind=leftouter (
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| summarize
LastSignInIP = arg_max(TimeGenerated, IPAddress),
LastSignInRisk = arg_max(TimeGenerated, RiskLevelDuringSignIn)
by UserPrincipalName
| project UserPrincipalName, LastSignInIP = IPAddress,
LastSignInRisk = RiskLevelDuringSignIn
) on $left.Actor == $right.UserPrincipalName
| project
TimeGenerated,
Actor,
OperationName,
Target,
LastSignInIP,
LastSignInRisk
| order by TimeGenerated desc
// Results: high-risk admin actions with the actor's most recent sign-in context
// An admin action from a user with a high-risk sign-in = investigate immediatelyCorrelating with OfficeActivity
The OfficeActivity table records user activity across M365 workloads — file access in SharePoint and OneDrive, mailbox operations in Exchange Online, and Teams activity. For identity security, the most critical correlations connect risky sign-ins with subsequent data access patterns.
Detect mass file download after a risky sign-in:
// EI1.10 — File download volume after risky sign-in
let riskyUsers = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| distinct UserPrincipalName, TimeGenerated;
OfficeActivity
| where TimeGenerated > ago(24h)
| where Operation in ("FileDownloaded", "FileSyncDownloadedFull")
| join kind=inner riskyUsers on $left.UserId == $right.UserPrincipalName
| where TimeGenerated > TimeGenerated1 // Download after the risky sign-in
| summarize
DownloadCount = count(),
DistinctFiles = dcount(OfficeObjectId),
Sites = make_set(Site_Url, 5)
by UserId
| where DownloadCount > 20 // Threshold: more than 20 downloads
// Results: users who downloaded many files after a risky sign-in
// This is the Stage 6 (data exfiltration) pattern from the identity kill chain// EI1.10 — Inbox rule creation correlated with risky sign-in
let riskyUsers = SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| distinct UserPrincipalName, TimeGenerated;
OfficeActivity
| where TimeGenerated > ago(24h)
| where Operation == "New-InboxRule"
| join kind=inner riskyUsers on $left.UserId == $right.UserPrincipalName
| where TimeGenerated > TimeGenerated1
| project
RiskySignInTime = TimeGenerated1,
InboxRuleTime = TimeGenerated,
User = UserId,
RuleParameters = Parameters,
MinutesAfterSignIn = datetime_diff('minute', TimeGenerated, TimeGenerated1)
// Results: inbox rules created after risky sign-ins
// This is the AiTM→BEC pattern from EI0.8 Case Study 1
// Every result requires immediate investigationTry it yourself
Try It — Correlate Sign-Ins with Audit Activity
Environment: Your M365 developer tenant with Sentinel workspace.
Exercise: First, generate some audit log activity by performing an administrative action in the Entra admin center — for example, create a new security group, or modify an existing user's display name. Wait 5-10 minutes for the event to appear in the AuditLogs table.
Then write a KQL query that joins your recent interactive sign-in (from SigninLogs) with the audit event you just generated (from AuditLogs). Join on your admin UPN and filter the audit event to occur after the sign-in. The result should show: your sign-in time, your audit action time, the operation you performed, and the minutes between the sign-in and the action.
This is the same correlation pattern used to detect post-compromise administrative activity — you are just using your own legitimate activity to practice the query.
The myth: Microsoft Sentinel correlates events across data sources automatically through its built-in detection rules and incident correlation. We do not need to write custom cross-table queries.
The reality: Sentinel's built-in analytics rules and fusion detection provide valuable automated correlation, but they are generic — designed to detect common patterns across all tenants. They do not know your organization's specific applications, user behavior patterns, named locations, or critical accounts. Custom cross-table queries tailored to your environment detect patterns that the built-in rules miss — like a specific service principal signing in from a new IP followed by a credential addition to a different service principal. EI13 builds these custom detection rules using the cross-table correlation patterns taught in this subsection. The built-in rules are a starting point; custom rules are the operational advantage.
You are reviewing NE's Entra ID security posture. You find 4 accounts with Global Administrator role, but NE's policy says maximum 2. The extra 2 were added during the AiTM incident for emergency response and never removed. Do you remove them?
Remove them — but through the proper process, not unilaterally. Notify the account owners that their emergency GA assignment is being revoked, confirm they have their standard role assignments restored, and document the removal with the rationale ('emergency assignment during INC-NE-2026-0227-001, no longer required'). Then add a PIR action item: 'Implement PIM time-limited role assignments for future incident response — emergency GA assignments auto-expire after 8 hours rather than persisting indefinitely.' The stale emergency assignment is a governance failure, not a technical failure — the fix is procedural.
You've mapped the identity threat landscape and learned to read sign-in logs.
EI0 established that every cloud attack starts with identity. EI1 took you through the signal that matters most — interactive, non-interactive, service principal, and managed identity sign-ins. Now you engineer the defences.
- 17 engineering modules — authentication methods, conditional access architecture, Identity Protection, PIM, token protection, application governance, and detection rules
- The Defense Design Method — the six-step framework applied to every identity control you'll build
- EI18 Capstone — Identity Security Architecture Design — design complete identity architectures for three realistic organisations (SMB, mid-market, regulated enterprise)
- Identity Security Toolkit lab pack — deployable conditional access policies, PIM configurations, and Identity Protection risk rules
- Cross-domain detection (EI16) — email-to-identity correlation and the full phishing-to-inbox-rule attack chain