Query Details

01 SIGNIN Password Spray

Query

id: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
name: "SigninLogs — Password Spray Attack (Single IP, Many Accounts)"
version: 1.0.0
kind: Scheduled
description: |
  Detects password spray attacks where a single IP address targets 10 or more distinct user accounts with authentication failures within a 1-hour window. Attackers deliberately stay under per-user lockout thresholds. MITRE ATT&CK: T1110.003 (Password Spraying).
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: PT1H
queryPeriod: PT1H
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
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 timeframe = 1h;
    let userThreshold = 10;
    let failureThreshold = 5;
    SigninLogs
    | invoke _ExcludeAllowlistedIPs()
    | where TimeGenerated > ago(timeframe)
    | where ResultType != "0"
    | summarize
        FailedAttempts = count(),
        UniqueUsers    = dcount(UserPrincipalName),
        TargetedUsers  = make_set(UserPrincipalName, 50),
        StartTime      = min(TimeGenerated),
        EndTime        = max(TimeGenerated),
        Apps           = make_set(AppDisplayName, 5),
        Locations      = make_set(Location, 3)
      by IPAddress
    | where UniqueUsers >= userThreshold and FailedAttempts >= failureThreshold
    | project
        TimeGenerated  = StartTime,
        IPAddress, UniqueUsers, FailedAttempts,
        TargetedUsers, EndTime, Apps, Locations
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  UniqueUsers: UniqueUsers
  FailedAttempts: FailedAttempts
alertDetailsOverride:
  alertDisplayNameFormat: "Password Spray from {{IPAddress}} — {{UniqueUsers}} accounts targeted"
  alertDescriptionFormat: "{{FailedAttempts}} failures from {{IPAddress}} targeting {{UniqueUsers}} accounts in 1 hour."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities:
  - IP



Explanation

This query is designed to detect password spray attacks, which occur when a single IP address attempts to log in to multiple user accounts using incorrect passwords within a short period. Here's a simplified breakdown of what the query does:

  1. Purpose: The query identifies potential password spray attacks by looking for instances where a single IP address has multiple failed login attempts across different user accounts within a one-hour window.

  2. Exclusion of Trusted IPs: It first excludes any IP addresses that are on a predefined allowlist, which includes trusted IPs or IP ranges. This helps in reducing false positives by ignoring known safe IPs.

  3. Timeframe and Thresholds: The query focuses on a one-hour timeframe and looks for IP addresses that have targeted 10 or more distinct user accounts with at least 5 failed login attempts.

  4. Data Source: It uses data from Azure Active Directory's SigninLogs to gather information about login attempts.

  5. Data Aggregation: For each IP address, it summarizes the number of failed attempts, the number of unique user accounts targeted, the specific users targeted, the time range of the attempts, the applications accessed, and the locations of the attempts.

  6. Alert Generation: If an IP address meets the criteria (10 or more unique users and 5 or more failed attempts), an alert is generated. The alert includes details like the number of accounts targeted and the number of failures from that IP address.

  7. Incident Management: The query is set up to create an incident if an alert is triggered, with configurations for grouping related alerts and managing incidents.

Overall, this query helps security teams identify and respond to potential password spray attacks by monitoring login failures from suspicious IP addresses.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogsUserIPAddressLocationAppDisplayName

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptymatchesregexextendiffhasstrcatsummarizemake_listtoscalarnotarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awayagoinvokedcountmake_setbyproject

Actions