Query Details

19 SIGNIN High Frequency Automated Sign Ins

Query

id: c9d0e1f2-a3b4-4c5d-6e7f-8a9b0c1d2e3f
name: "SigninLogs — High-Frequency Repeated Sign-Ins (Automated Credential Abuse)"
version: 1.0.0
kind: Scheduled
description: |
  Detects accounts producing 10+ successful sign-in events per hour from the same IP. At that frequency, activity is almost certainly generated by an automated tool, a script, or a C2 framework maintaining stolen token freshness. MITRE ATT&CK: T1078 (Valid Accounts), T1539 (Steal Web Session Cookie).
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: PT1H
queryPeriod: PT1H
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1539
  - T1528
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 loginMinimum = 10;
    SigninLogs
    | invoke _ExcludeAllowlistedIPs()
    | where TimeGenerated > ago(1h)
    | where ResultType == "0"
    | where isnotempty(IPAddress) and isnotempty(UserPrincipalName)
    | summarize
        LoginCount  = count(),
        UniqueApps  = dcount(AppDisplayName),
        Apps        = make_set(AppDisplayName, 10),
        Locations   = make_set(Location, 3),
        FirstLogin  = min(TimeGenerated),
        LastLogin   = max(TimeGenerated)
      by UserPrincipalName, IPAddress
    | where LoginCount >= loginMinimum
    | extend
        WindowMinutes   = datetime_diff("minute", LastLogin, FirstLogin),
        LoginFreqPerMin = round(
            todouble(LoginCount) /
            (todouble(datetime_diff("second", LastLogin, FirstLogin)) / 60.0 + 1.0), 2)
    | project
        TimeGenerated = FirstLogin,
        UserPrincipalName, IPAddress,
        LoginCount, UniqueApps,
        LoginFreqPerMin, WindowMinutes,
        Apps, Locations, LastLogin
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  LoginCount: LoginCount
  LoginFreqPerMin: LoginFreqPerMin
alertDetailsOverride:
  alertDisplayNameFormat: "Automated Sign-Ins — {{UserPrincipalName}} from {{IPAddress}} — {{LoginCount}} logins/hr"
  alertDescriptionFormat: "Account {{UserPrincipalName}} produced {{LoginCount}} successful sign-ins/hour from {{IPAddress}} — automated tooling or C2 token refresh pattern."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities:
  - Account
  - IP



Explanation

This query is designed to detect potential automated credential abuse by identifying accounts that have more than 10 successful sign-ins per hour from the same IP address. Here's a simplified breakdown of what the query does:

  1. Purpose: The query aims to identify suspicious sign-in activity that could indicate automated tools or scripts are being used to maintain access to an account, potentially using stolen credentials.

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

  3. Frequency: The query runs every hour and looks at sign-in data from the past hour.

  4. Exclusions: It excludes sign-ins from trusted IP addresses or ranges, which are specified in a "NetworkAllowlist."

  5. Detection Logic:

    • It checks for successful sign-ins (ResultType "0").
    • It counts the number of sign-ins per user and IP address.
    • It calculates the frequency of sign-ins per minute.
    • It flags any account with 10 or more sign-ins from the same IP address within an hour.
  6. Output: The query provides details such as the user's principal name, IP address, number of logins, unique applications accessed, and locations.

  7. Alerting: If the conditions are met, an alert is generated with a display name and description indicating the user, IP address, and number of logins per hour. It also creates an incident for further investigation.

  8. Relevance: This query aligns with MITRE ATT&CK techniques T1078 (Valid Accounts) and T1539 (Steal Web Session Cookie), indicating its focus on credential access and persistence tactics.

Overall, this query helps security teams identify and respond to potential automated attacks on user accounts by monitoring for high-frequency sign-ins from the same IP address.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogsAzureActiveDirectoryAccountIPAddressUserPrincipalNameAppDisplayNameLocationTimeGenerated

Operators

letmaterializeunionprinttakeprojectwhereisnotemptymatches regexextendiffstrcatsummarizemake_listtoscalarnotipv4_is_in_any_rangemv-applysplitipv4_comparemaxtointproject-awayinvokeagocountdcountmake_setminmaxbydatetime_diffroundtodoubleproject

Actions