14.9 Detection Engineering
3-5 hours · Module 14
Detection Engineering
Five analytics rules that detect token replay patterns. These complement the AiTM detection rules (M12.13) and BEC detection rules (M13.10) — together they cover the full attack chain from credential theft through token abuse to financial fraud.
Required role: Microsoft Sentinel Contributor.
Rule 1: Multiple IPs Within Single User Session Window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Scheduled Rule: Same user, successful sign-ins from 2+ IPs within 10 minutes
// Schedule: 5 minutes / Lookback: 15 minutes
// Severity: High
// MITRE: T1550.001 (Application Access Token)
let KnownCorpIPs = _GetWatchlist('CorporateExternalIPs') | project SearchKey;
SigninLogs
| where TimeGenerated > ago(15m)
| where ResultType == "0"
| where IPAddress !in (KnownCorpIPs)
| summarize
IPs = make_set(IPAddress, 10),
IPCount = dcount(IPAddress),
Apps = make_set(AppDisplayName, 5),
FirstSignin = min(TimeGenerated),
LastSignin = max(TimeGenerated)
by UserPrincipalName
| where IPCount > 1
| extend GapMinutes = datetime_diff('minute', LastSignin, FirstSignin)
| where GapMinutes < 10
| project UserPrincipalName, IPs, IPCount, GapMinutes, Apps
|
Entity mapping: Account → UserPrincipalName (FullName).
Custom details: IPs, IPCount, GapMinutes.
Rule 2: Non-Interactive Sign-In from Non-Corporate IP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Scheduled Rule: Non-interactive auth from external IP — token replay indicator
// Schedule: 15 minutes / Lookback: 20 minutes
// Severity: Medium
// MITRE: T1550.001
let KnownCorpIPs = _GetWatchlist('CorporateExternalIPs') | project SearchKey;
AADNonInteractiveUserSignInLogs
| where ResultType == "0"
| where IPAddress !in (KnownCorpIPs)
| where not(ipv4_is_private(IPAddress))
| extend UserAgent = tostring(DeviceDetail.browser)
// Filter for suspicious user agents (scripted access)
| where UserAgent has_any ("python", "curl", "wget", "PowerShell",
"Go-http-client", "axios", "node-fetch")
or isempty(UserAgent)
| project TimeGenerated, UserPrincipalName, IPAddress,
AppDisplayName, ResourceDisplayName, UserAgent
|
Entity mapping: Account → UserPrincipalName (FullName). IP → IPAddress (Address).
Why this rule matters: Scripted access from external IPs is almost always token abuse. Legitimate M365 applications use recognisable user agents. Python/curl/empty user agents from non-corporate IPs are attacker tools.
Rule 3: Token Used After Password Reset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Scheduled Rule: Successful sign-in after password reset without new MFA
// Schedule: 15 minutes / Lookback: 1 hour
// Severity: High
// MITRE: T1550.001
let PasswordResets = AuditLogs
| where OperationName has "Reset password"
| where Result == "success"
| extend ResetUser = tostring(TargetResources[0].userPrincipalName)
| extend ResetTime = TimeGenerated
| project ResetUser, ResetTime;
SigninLogs
| where ResultType == "0"
| where AuthenticationRequirement == "singleFactorAuthentication"
| join kind=inner PasswordResets
on $left.UserPrincipalName == $right.ResetUser
| where TimeGenerated > ResetTime
| where TimeGenerated < (ResetTime + 2h)
| project UserPrincipalName, ResetTime, SigninTime = TimeGenerated,
IPAddress, AppDisplayName, AuthenticationRequirement,
MinutesAfterReset = datetime_diff('minute', TimeGenerated, ResetTime)
|
What this detects: A successful sign-in that occurs AFTER a password reset but does NOT require MFA. This means the sign-in used a pre-existing token (issued before the reset) — the token survived the password change. If the user genuinely re-authenticated with their new password, MFA would be required. Single-factor auth after reset = token replay.
Rule 4: Refresh Token Usage from New Device
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Scheduled Rule: Non-interactive sign-in from device ID not seen in 90 days
// Schedule: 1 hour / Lookback: 1 hour 15 minutes
// Severity: Medium
// MITRE: T1528 (Steal Application Access Token)
let KnownDevices = AADNonInteractiveUserSignInLogs
| where TimeGenerated between(ago(90d) .. ago(1d))
| where ResultType == "0"
| summarize KnownDeviceIds = make_set(tostring(DeviceDetail.deviceId), 100)
by UserPrincipalName;
AADNonInteractiveUserSignInLogs
| where ResultType == "0"
| extend DeviceId = tostring(DeviceDetail.deviceId)
| where isnotempty(DeviceId)
| join kind=inner KnownDevices on UserPrincipalName
| where DeviceId !in (KnownDeviceIds)
| project TimeGenerated, UserPrincipalName, IPAddress,
DeviceId, AppDisplayName, ResourceDisplayName
|
Rule 5: Session Anomaly — High-Volume Resource Access from Single Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Scheduled Rule: Anomalous resource access volume — token being used for bulk operations
// Schedule: 1 hour / Lookback: 1 hour 15 minutes
// Severity: Medium
// MITRE: T1078.004 (Valid Accounts: Cloud Accounts)
AADNonInteractiveUserSignInLogs
| where ResultType == "0"
| where IPAddress !in (_GetWatchlist('CorporateExternalIPs') | project SearchKey)
| summarize
ResourceCount = dcount(ResourceDisplayName),
Resources = make_set(ResourceDisplayName, 10),
SigninCount = count()
by UserPrincipalName, IPAddress
| where ResourceCount > 5 and SigninCount > 20
| project UserPrincipalName, IPAddress, ResourceCount, SigninCount, Resources
|
What this detects: A single IP accessing many different resources for one user in a short period. Legitimate users typically access 2-3 resources per hour. An attacker enumerating all available resources with a stolen token accesses 5+ resources in rapid succession.
Deployment checklist
For each rule: create in Sentinel Analytics, configure entity mapping, set MITRE technique, test against 30 days data, enable, monitor 7 days.
Subsection artifact: 5 deployable token replay detection rules. These complement the M11 AiTM rules and M12 BEC rules — together, the three detection packs cover the complete attack chain.
Compliance mapping: NIST CSF DE.AE-2 (Anomalous activity detected). ISO 27001 A.8.16 (Monitoring).