Query Details

11 SIGNIN Slow Low Password Spray

Query

id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
name: "SigninLogs — Slow & Low Password Spray (Multi-Day Evasion)"
version: 1.0.0
kind: Scheduled
description: |
  Detects password spray campaigns deliberately spread over multiple days to evade per-hour detection thresholds. Identifies a single IP targeting 3+ distinct accounts per day across 3+ separate days. Classic TTP from adversary groups using purchased credential lists at low throttle rates. MITRE ATT&CK: T1110.003 (Password Spraying).
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: P1D
queryPeriod: P7D
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 lookback          = 7d;
    let minAccountsPerDay = 3;
    let minActiveDays     = 3;
    SigninLogs
    | invoke _ExcludeAllowlistedIPs()
    | where TimeGenerated > ago(lookback)
    | where ResultType != "0"
    | where isnotempty(IPAddress)
    | extend Day = bin(TimeGenerated, 1d)
    | summarize
        AccountsOnDay = dcount(UserPrincipalName),
        FailuresOnDay = count()
      by IPAddress, Day
    | where AccountsOnDay >= minAccountsPerDay
    | summarize
        DaysActive    = dcount(Day),
        TotalAccounts = sum(AccountsOnDay),
        TotalFailures = sum(FailuresOnDay),
        ActiveDays    = make_set(Day, 10),
        StartDay      = min(Day),
        EndDay        = max(Day)
      by IPAddress
    | where DaysActive >= minActiveDays
    | project
        TimeGenerated = EndDay,
        IPAddress,
        DaysActive,
        TotalAccounts,
        TotalFailures,
        StartDay,
        EndDay,
        ActiveDays
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  DaysActive: DaysActive
  TotalAccounts: TotalAccounts
  TotalFailures: TotalFailures
alertDetailsOverride:
  alertDisplayNameFormat: "Slow & Low Spray from {{IPAddress}} — {{DaysActive}} active days, {{TotalAccounts}} accounts"
  alertDescriptionFormat: "IP {{IPAddress}} spread {{TotalAccounts}} targeted accounts across {{DaysActive}} active days — multi-day evasion of per-hour spray detection."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
    groupByEntities:
  - IP



Explanation

This query is designed to detect a specific type of cyber attack known as a "password spray" campaign. Here's a simplified breakdown of what it does:

  1. Purpose: The query identifies password spray attacks that are spread over multiple days. These attacks are designed to evade detection systems that typically monitor for rapid, high-volume login attempts within a short time frame.

  2. Detection Criteria:

    • It looks for a single IP address attempting to log into at least 3 different accounts per day.
    • This activity must occur over at least 3 separate days within a 7-day period.
  3. Exclusions:

    • The query excludes login attempts from trusted IP addresses or ranges, which are defined in a "Network Allowlist."
  4. Data Source:

    • It uses data from Azure Active Directory's SigninLogs to track login attempts.
  5. Output:

    • The query outputs details such as the IP address involved, the number of days the activity was detected, the total number of accounts targeted, and the total number of failed login attempts.
  6. Alerting:

    • If the criteria are met, an alert is generated with a custom display name and description, highlighting the IP address and the extent of the activity.
    • An incident is created for further investigation, and similar incidents can be grouped together based on the IP address.
  7. Severity and Techniques:

    • The severity of this detection is marked as "High."
    • It relates to the MITRE ATT&CK technique T1110.003, which is associated with password spraying.

In essence, this query helps security teams identify and respond to stealthy password spray attacks that are deliberately slow and spread out over time to avoid detection.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogs

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendhasstrcatsummarizemake_listiffarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awayagoinvokebindcountcountsummake_setminmaxbyproject

Actions