Query Details

12 SIGNIN Account Enumeration

Query

id: b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e
name: "SigninLogs — Account Enumeration via Error Code Fingerprinting"
version: 1.0.0
kind: Scheduled
description: |
  Detects attackers performing user enumeration by analysing sign-in error codes. Error 50034 (UserNotFound) reveals non-existent accounts; error 50126 confirms the account exists. A high ratio of 50034 errors indicates systematic enumeration of valid users before a targeted spray. MITRE ATT&CK: T1087.002 (Domain Account Discovery), T1110 (Brute Force).
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: PT6H
queryPeriod: PT6H
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - CredentialAccess
relevantTechniques:
  - T1087
  - 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      = 6h;
    let minAttempts   = 20;
    let enumRatioMin  = 0.40;
    SigninLogs
    | invoke _ExcludeAllowlistedIPs()
    | where TimeGenerated > ago(lookback)
    | where ResultType in ("50034", "50126", "50057", "70011")
    | where isnotempty(IPAddress)
    | summarize
        TotalAttempts   = count(),
        UserNotFound    = countif(ResultType == "50034"),
        InvalidPassword = countif(ResultType == "50126"),
        AccountDisabled = countif(ResultType == "50057"),
        UniqueTargets   = dcount(UserPrincipalName),
        TargetedUsers   = make_set(UserPrincipalName, 30),
        Locations       = make_set(Location, 3),
        FirstAttempt    = min(TimeGenerated),
        LastAttempt     = max(TimeGenerated)
      by IPAddress
    | where TotalAttempts >= minAttempts
    | extend EnumRatio = round(todouble(UserNotFound) / TotalAttempts, 2)
    | where EnumRatio >= enumRatioMin
    | project
        TimeGenerated   = FirstAttempt,
        IPAddress,
        TotalAttempts,
        UserNotFound,
        InvalidPassword,
        UniqueTargets,
        EnumRatio,
        TargetedUsers,
        Locations,
        LastAttempt
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  TotalAttempts: TotalAttempts
  UserNotFound: UserNotFound
  EnumRatio: EnumRatio
alertDetailsOverride:
  alertDisplayNameFormat: "Account Enumeration from {{IPAddress}} — {{EnumRatio}} UserNotFound ratio ({{TotalAttempts}} probes)"
  alertDescriptionFormat: "IP {{IPAddress}} probed accounts with {{EnumRatio}} UserNotFound ratio — user enumeration pattern before a targeted spray attack."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT6H
    matchingMethod: AllEntities
    groupByEntities:
  - IP



Explanation

This query is designed to detect potential attackers attempting to discover valid user accounts by analyzing sign-in error codes in Azure Active Directory logs. Here's a simplified explanation:

  1. Purpose: The query identifies suspicious activity where attackers try to find valid user accounts by checking sign-in error codes. Specifically, it looks for error code 50034 (UserNotFound) which indicates non-existent accounts, and error code 50126 which confirms the account exists.

  2. Severity: The query is marked with high severity because it can indicate a precursor to a more targeted attack, such as a password spray attack.

  3. Data Source: It uses data from Azure Active Directory's SigninLogs.

  4. Frequency: The query runs every 6 hours and looks at data from the past 6 hours.

  5. Exclusion of Trusted IPs: It excludes IP addresses that are on a predefined allowlist, which includes trusted IP ranges.

  6. Detection Logic:

    • It checks for a minimum of 20 sign-in attempts from a single IP address.
    • It calculates the ratio of UserNotFound errors to total attempts. If this ratio is 40% or higher, it flags the activity as suspicious.
    • It summarizes the data by IP address, counting total attempts, number of UserNotFound errors, and other details like unique user targets and locations.
  7. Alerting: If suspicious activity is detected, an alert is generated with details about the IP address, the ratio of UserNotFound errors, and the total number of attempts.

  8. Incident Management: If an alert is triggered, an incident is created for further investigation, and similar incidents are grouped by IP address.

Overall, this query helps in identifying and alerting on potential user enumeration attacks, which are often a precursor to more serious credential-based attacks.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogsAzureActiveDirectoryUserIPAddressLocationTimeGeneratedUserPrincipalName

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listnotarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofonsplitipv4_comparemaxtointproject-awayagoininvokecountcountifdcountmake_setminmaxbyroundtodoubleproject

Actions