Query Details

14 SIGNIN Password Reset Then Foreign Sign In

Query

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



Explanation

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:

  1. 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.

  2. Severity: The alert generated by this query is considered high severity, indicating a strong likelihood of account compromise.

  3. Data Sources: It uses data from Azure Active Directory's SigninLogs and AuditLogs to track password resets and sign-in activities.

  4. Network Allowlist: The query excludes sign-ins from trusted IP addresses or ranges, which are defined in a network allowlist, to reduce false positives.

  5. Process:

    • It first identifies password reset events within the last two hours.
    • It then checks for successful sign-ins from new countries (not seen in the last 30 days) within two hours after the password reset.
    • It ensures that these sign-ins are not from allowlisted IPs.
  6. 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.

  7. 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.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogsAuditLogs

Keywords

SigninLogsAuditLogsUserPrincipalNameIPAddressLocationAppDisplayNameRiskLevelDuringSignInAccountIP

Operators

letmaterializeunionisfuzzyprinttakeprojectwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listtostringarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awayagointolowerdistinctjoinkindinnerbetweenon$left$rightleftantidatetime_diff

Actions