Banner

Detecting Risky Password Reset Activity in Microsoft Sentinel

Correlating Cloud and On-Premises Signals to Catch Multi-Reset Attempts

Category Identity Security · Microsoft Sentinel · KQL

Password resets are a normal part of day-to-day life in any environment: users forget credentials, policies enforce rotation, and self-service flows help reduce helpdesk load. But when an account starts generating a burst of reset attempts in a short window, the story often changes from “user convenience” to “identity under active attack”.

In this post I’ll walk through a Microsoft Sentinel analytic rule that uses KQL to detect suspicious patterns of multiple password reset attempts per user, correlating signals from:

Goal: flag accounts that exceed a configurable threshold of password change or reset attempts in a short period of time, while filtering out expected operational noise such as sync accounts and routine self-service password reset flows.

Why multiple password reset attempts are risky

A single password change in isolation is rarely interesting. A cluster of changes or resets for the same user within seconds usually is. Some common scenarios:

Looking at any one log source in isolation makes it easy to miss these patterns. The query below unifies multiple sources and focuses on bursts of activity per user, not just individual events.


Detection overview

The analytic rule is built around a few design principles:

The core pieces:

AuditLogs SecurityEvent 4723 / 4724 Syslog (auth, authpriv) Watchlist: AD-Sync-Accounts row_window_session

The KQL analytic rule

The following query is intended to run as an analytic rule in Microsoft Sentinel. It correlates password reset activity across your hybrid identity stack and highlights accounts with multiple reset attempts above a defined threshold.


    let knownServiceAccount = (_GetWatchlist('AD-Sync-Accounts'))
        | project SearchKey;
    let selfServicePasswordReset = dynamic([
            "Self-service password reset flow activity progress",
            "Change password (self-service)",
            "Reset password (self-service)"
            ]); 
    // Self-service password reset flow activity progress is typically caused by a password policy which requires users to rotate passwords.
    // This operation already implies the user has signed in successfully and therefore the password reset is non-malicious.
    let PerUserThreshold = 5;
    let action = dynamic(["change", "changed", "reset"]);
    let pWord = dynamic(["password", "credentials"]);
    let SSPR = AuditLogs
    | where OperationName has_any (pWord)
        and OperationName has_any (action)
        and Result =~ "success"
    | where OperationName !in (selfServicePasswordReset)
    | mv-apply TargetResource = TargetResources on (
        where TargetResource.type =~ "User"
        | extend AccountType = tostring(TargetResource.type),
            Account = tostring(InitiatedBy.user.userPrincipalName),
            TargetUserName = tolower(tostring(TargetResource.userPrincipalName))
    )
    | where not(Account has_any (knownServiceAccount))
    | project TimeGenerated, AccountType, Account, TargetUserName, Computer = "", Type, OperationName;
    let OnPremisesReset = 
        union isfuzzy=true
        (//Password reset events
        //4723: An attempt was made to change an account's password
        //4724: An attempt was made to reset an accounts password
        SecurityEvent
        | where EventID in ("4723","4724")
        | where not(Account has_any (knownServiceAccount))
        | extend Account = tostring(split(Account, "\\")[-1])
        | project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
        (// Unix syslog password reset events
        Syslog
        | where Facility in ("auth","authpriv")
        | where SyslogMessage has_any (pWord) and SyslogMessage has_any (action)
        | extend AccountType = iif(SyslogMessage contains "root", "Root", "Non-Root")
        | where SyslogMessage matches regex ".*password changed for.*"
        | parse SyslogMessage with * "password changed for" Account
        | extend TargetAccount = Account
        | project TimeGenerated, AccountType, Account, Computer = HostName, Type)
    | where Account has TargetUserName and isnotempty(TargetUserName)
    ;
    union isfuzzy=true SSPR, OnPremisesReset
    | where Account =~ TargetUserName
    | sort by TimeGenerated asc, Account asc
    | extend AttemptStart = row_window_session(TimeGenerated, 25s, 25s, Account != prev(Account))
    | summarize 
        StartTimeUtc = min(TimeGenerated),
        EndTimeUtc = max(TimeGenerated),
        Computerlist = make_set(Computer, 25),
        Computer = arg_max(Computer , TimeGenerated),
        AccountType = make_set(AccountType, 25),
        TargetUserName = arg_max(TargetUserName, TimeGenerated),
        AttemptCount = dcount(AttemptStart) by Account
    | order by TimeGenerated desc
    | where AttemptCount > PerUserThreshold
    | extend timestamp = StartTimeUtc,
        HostName = tostring(split(Computer, '.', 0)[0]),
        DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')),
        Name = tostring(split(Account, '@', 0)[0]),
        UPNSuffix = tostring(split(Account, '@', 1)[0]),
        TargetName = tostring(split(TargetUserName,'@',0)[0]),
        TargetUPNSuffix = tostring(split(TargetUserName,'@',1)[0])
        

How the detection works

1. Ignore noisy service accounts

The query starts by pulling a watchlist named AD-Sync-Accounts and projecting only the SearchKey field:

Keeping them out reduces false positives and keeps the output focused on user-driven activity.

2. Separate “expected” self-service flows

Not all password changes are suspicious. The selfServicePasswordReset array captures specific operation names that typically indicate user-initiated, policy-driven flows such as:

These are explicitly removed from the AuditLogs portion to avoid alerting on routine, successful self-service activity.

3. Collect cloud password reset events (SSPR)

The SSPR let block:

At this stage, we have a clean, cloud-side view of successful password operations initiated by real users.

4. Collect on-premises and Unix resets

The OnPremisesReset block uses a fuzzy union to combine two worlds:

A few notable steps:

After both branches are normalized, the query keeps only events where the Account field matches a valid TargetUserName, ensuring we’re looking at meaningful identity-level activity.

5. Correlate across sources and align on the user

The query then:

At this point, you have a time-ordered stream of password-related activity per account across cloud and on-prem sources.

6. Detect “bursts” of attempts with row_window_session

This is where the multi-attempt detection happens:


    | extend AttemptStart = row_window_session(
        TimeGenerated,
        25s,
        25s,
        Account != prev(Account)
    )
        

row_window_session groups events into logical sessions based on:

Multiple reset events that fall within this short window for the same account are treated as a single “attempt session”. Later, the query counts distinct sessions per account via dcount(AttemptStart) to derive AttemptCount.

7. Summarize per user and apply a threshold

The summarize step aggregates everything per Account:

Only accounts with AttemptCount > PerUserThreshold are kept. With a threshold of 5, you’re focusing on users that had more than five distinct reset clusters in the time window of your rule.

Finally, the query enriches identities by splitting:


Tuning and operational guidance

Thresholds and time windows

Expanding the watchlist

The AD-Sync-Accounts watchlist is a natural place to maintain “known noisy” identities:

Keep the list small and intentional; over-excluding will hide genuine risk.

Incident triage tips

When this rule fires, a few quick checks help prioritize:

Combining this detection with sign-in risk signals and device health can quickly distinguish “user fat-fingering their password” from “active account takeover”.


Closing thoughts

Password resets are more than just a user convenience feature—they’re a powerful signal for identity-centric detection when you look at them in context. By correlating reset attempts across Azure AD, on-prem Windows logs, and Unix syslog, and by focusing on per-user clusters of activity instead of single events, this KQL analytic rule helps surface accounts that are very likely under attack.

If you run Microsoft Sentinel in a hybrid environment, consider deploying and tuning this detection as part of your broader identity threat hunting and incident response playbook.

Shoutout to Guus Verbeek for pointing out a minor issue in the splitting of the Account for On-Premises password changes. This issue is fixed in the KQL.