← Back to SOC feed Coverage →

User Accounts - Successful Sign in Spikes

kql MEDIUM Azure-Sentinel
T1078.004
AADNonInteractiveUserSignInLogsSigninLogs
backdoorhuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-06-04T23:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be creating new user accounts to establish persistent access or exfiltrate data by triggering abnormal sign-in spikes. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential account compromise or lateral movement tactics.

KQL Query

let starttime = 14d;
let timeframe = 1d;
let scorethreshold = 5;
let baselinethreshold = 25;
let aadFunc = (tableName:string){
  // Succesful signins.
  table(tableName)
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(timeframe)))
  | where ResultType == 0
  | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let allSignins = union isfuzzy=true aadSignin, aadNonInt ;
let TimeSeriesData = union isfuzzy=true aadSignin, aadNonInt 
| project TimeGenerated, UserPrincipalName
| make-series HourlyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step timeframe by UserPrincipalName
| project  TimeGenerated, UserPrincipalName, HourlyCount;
let TimeSeriesAlerts = TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(HourlyCount, scorethreshold, -1, 'linefit')
| mv-expand HourlyCount to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend AnomalyHour = TimeGenerated
| where baseline > baselinethreshold // Filtering low count events per baselinethreshold
| project UserPrincipalName, AnomalyHour, TimeGenerated, HourlyCount, baseline, anomalies, score;
let AnomalyHours = TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated;
// Filter the alerts for specified timeframe
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join kind=inner ( 
union isfuzzy=true aadSignin, aadNonInt
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
 | summarize HourlyCount=count(), LatestAnomalyTime = arg_max(timestamp,*) by bin(TimeGenerated,1h),  OperationName, Category, ResultType, ResultDescription, UserPrincipalName, UserDisplayName, AppDisplayName, ClientAppUsed, IPAddress, ResourceDisplayName
) on UserPrincipalName
| project LatestAnomalyTime,  OperationName, Category, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, AppDisplayName, ClientAppUsed, UserAgent, IPAddress, Location, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, HourlyCount, baseline, anomalies, score
| extend timestamp = LatestAnomalyTime, IPCustomEntity = IPAddress, AccountCustomEntity = UserPrincipalName

Analytic Rule Definition

id: 3c7fcea1-ec9f-4ea2-a555-156073b2d183
name: User Accounts - Successful Sign in Spikes
description: |
  ' Identifies measureable increase in successful sign-ins from user accounts.
  Spike is determined based on Time series anomaly which will look at historical baseline values.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-successful-unusual-sign-ins'
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let starttime = 14d;
  let timeframe = 1d;
  let scorethreshold = 5;
  let baselinethreshold = 25;
  let aadFunc = (tableName:string){
    // Succesful signins.
    table(tableName)
    | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(timeframe)))
    | where ResultType == 0
    | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  let allSignins = union isfuzzy=true aadSignin, aadNonInt ;
  let TimeSeriesData = union isfuzzy=true aadSignin, aadNonInt 
  | project TimeGenerated, UserPrincipalName
  | make-series HourlyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step timeframe by UserPrincipalName
  | project  TimeGenerated, UserPrincipalName, HourlyCount;
  let TimeSeriesAlerts = TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(HourlyCount, scorethreshold, -1, 'linefit')
  | mv-expand HourlyCount to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
  | where anomalies > 0 | extend AnomalyHour = TimeGenerated
  | where baseline > baselinethreshold // Filtering low count events per baselinethreshold
  | project UserPrincipalName, AnomalyHour, TimeGenerated, HourlyCount, baseline, anomalies, score;
  let AnomalyHours = TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated;
  // Filter the alerts for specified timeframe
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join kind=inner ( 
  union isfuzzy=true aadSignin, aadNonInt
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
   | summarize HourlyCount=count(), LatestAnomalyTime = arg_max(timestamp,*) by bin(TimeGenerated,1h),  OperationName, Category, ResultType, ResultDescription, UserPrincipalName, UserDisplayName, AppDisplayName, ClientAppUsed, IPAddress, ResourceDisplayName
  ) on UserPrincipalName
  | project LatestAnomalyTime,  OperationName, Category, UserPrincipalName, UserDisplayName, ResultType, R

Required Data Sources

Sentinel TableNotes
AADNonInteractiveUserSignInLogsEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/SigninLogs/UserAccountsMeasurableincreaseofsuccessfulsignins.yaml