Query Details
id: d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a
name: "SigninLogs — Password Reset Followed by New-Country Sign-In (Account Takeover)"
version: 1.0.0
kind: Scheduled
description: |
Detects a high-confidence account takeover pattern: a password reset followed within 2 hours by a successful sign-in from a country not seen in the prior 30 days. This chain strongly indicates an attacker reset the victim password via a phishing-obtained session and immediately used the new credential. MITRE ATT&CK: T1078 (Valid Accounts), T1098 (Account Manipulation).
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: PT2H
queryPeriod: P1D
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1078
- T1098
query: |
// ---- Network Allowlist (exclude trusted IPs / CIDR / ranges) --------------
let _allow = materialize(union isfuzzy=true (print R="" | take 0), (_GetWatchlist('NetworkAllowlist') | project R = tostring(IPOrRange)) | where isnotempty(R));
let _allowCIDR = toscalar(_allow | where not(R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$') | extend R = iff(R has '/', R, strcat(R, '/32')) | summarize make_list(R));
let _allowRange = toscalar(_allow | where R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$' | summarize make_list(R));
let _ExcludeAllowlistedIPs = (T:(IPAddress:string)) {
T
| extend IPAddress = tostring(IPAddress)
| where array_length(_allowCIDR) == 0 or isnull(ipv4_is_in_any_range(IPAddress, _allowCIDR)) or not(ipv4_is_in_any_range(IPAddress, _allowCIDR))
| mv-apply _r = _allowRange to typeof(string) on (
extend _lo = tostring(split(_r,'-')[0]), _hi = tostring(split(_r,'-')[1])
| extend _inRange = ipv4_compare(IPAddress, _lo) >= 0 and ipv4_compare(IPAddress, _hi) <= 0
| summarize _anyInRange = max(toint(_inRange)))
| where isnull(_anyInRange) or _anyInRange == 0
| project-away _anyInRange
};
// ---------------------------------------------------------------------------
let lookback = 2h;
let baselinePeriod = 30d;
let resetWindow = 2h;
let ResetEvents =
AuditLogs
| where TimeGenerated > ago(lookback + resetWindow)
| where OperationName in (
"Reset password (self-service)",
"Reset user password",
"Admin reset user password"
)
| extend TargetUPN = tolower(tostring(TargetResources[0].userPrincipalName))
| where isnotempty(TargetUPN)
| project ResetTime = TimeGenerated, TargetUPN;
let KnownLocations =
SigninLogs
| invoke _ExcludeAllowlistedIPs()
| where TimeGenerated between (ago(baselinePeriod) .. ago(lookback))
| where ResultType == "0"
| where isnotempty(Location) and Location != "Unknown"
| distinct UserPrincipalName, Location;
ResetEvents
| join kind=inner (
SigninLogs
| invoke _ExcludeAllowlistedIPs()
| where TimeGenerated > ago(lookback + resetWindow)
| where ResultType == "0"
| where isnotempty(Location) and Location != "Unknown"
| extend UserPrincipalName = tolower(UserPrincipalName)
| project
UserPrincipalName, SignInTime = TimeGenerated,
IPAddress, Location, AppDisplayName,
RiskLevelDuringSignIn
) on $left.TargetUPN == $right.UserPrincipalName
| where SignInTime > ResetTime
| where SignInTime - ResetTime <= resetWindow
| join kind=leftanti KnownLocations on UserPrincipalName, Location
| extend MinutesSinceReset = datetime_diff("minute", SignInTime, ResetTime)
| project
TimeGenerated = SignInTime,
UserPrincipalName, IPAddress, Location,
AppDisplayName, RiskLevelDuringSignIn,
ResetTime, MinutesSinceReset
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
customDetails:
Location: Location
MinutesSinceReset: MinutesSinceReset
alertDetailsOverride:
alertDisplayNameFormat: "Account Takeover — {{UserPrincipalName}} signed in from {{Location}} {{MinutesSinceReset}}m after reset"
alertDescriptionFormat: "Account {{UserPrincipalName}} authenticated from a new country ({{Location}}) only {{MinutesSinceReset}} minutes after a password reset — strong indicator of account takeover."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT4H
matchingMethod: AllEntities
groupByEntities:
- Account
- IP
This query is designed to detect potential account takeover incidents in an organization's Azure Active Directory environment. Here's a simplified explanation of what it does:
Purpose: The query identifies cases where a user's password is reset, and then within two hours, there is a successful sign-in from a country that hasn't been seen in the last 30 days for that user. This pattern suggests that an attacker might have reset the password using a phishing-obtained session and then used the new credentials to access the account.
Severity: The alert generated by this query is considered high severity, indicating a strong likelihood of account compromise.
Data Sources: It uses data from Azure Active Directory's SigninLogs and AuditLogs to track password resets and sign-in activities.
Network Allowlist: The query excludes sign-ins from trusted IP addresses or ranges, which are defined in a network allowlist, to reduce false positives.
Process:
Alert Details: If such an event is detected, an alert is generated with details like the user's principal name, IP address, location, and the time difference between the password reset and the sign-in.
Incident Management: The query is configured to create an incident in the security system, grouping related alerts by account and IP to help security teams manage and investigate potential threats efficiently.
Overall, this query helps security teams quickly identify and respond to potential account takeover incidents by highlighting suspicious patterns of password resets followed by unusual sign-in activities.

David Alonso
Released: April 20, 2026
Tables
Keywords
Operators