Multiple Potential MFA Request Spam
Query
// This query is too long to be in an Analytics Rule (more than 10.000 characters), so it had to be made a function that can be called by the rule.
// You can find the function in the next link, just try to define "query_frequency" and "query_period".
//
// https://github.com/ep3p/Sentinel_KQL/blob/main/Functions/Analytics-PotentialMFASpam.kql
//
PotentialMFASpam(query_frequency = 1h, query_period = 14d)Explanation
This query is designed to detect potential Multi-Factor Authentication (MFA) spam. It's too long to fit into an Analytics Rule (which has a limit of 10,000 characters), so it has been converted into a function that can be called by the rule. The function is hosted on GitHub and can be accessed via the provided link.
The function, PotentialMFASpam, takes two parameters: query_frequency and query_period. Query_frequency determines how often the query is run (for example, every hour) and query_period defines the time period that the query covers (for example, the past 14 days).
Details

Jose Sebastián Canós
Released: September 7, 2023
Tables
The query does not provide information about the table or tables used.
Keywords
AnalyticsPotentialMFASpamQuery FrequencyQuery PeriodFunctionRuleCharactersSentinel KQLGitHub
Operators
| summarize count() by UserPrincipalNameCallerIPAddressResultTypeResultDescription
| where count_ > 1
| summarize arg_max(count_*) by UserPrincipalNameCallerIPAddress
| mv-expand todynamic(CallerIPAddress)
| extend ip = tostring(CallerIPAddress)
| summarize count() by UserPrincipalNameip
| where count_ > 1
| summarize arg_max(count_*) by UserPrincipalName
| extend user = tostring(UserPrincipalName)
| summarize count() by user
| where count_ > 1
| summarize arg_max(count_*) by user
| extend user = tostring(user)
| project user
| invoke GetIPInfo(ip)
| extend city = tostring(LocationData[0].city)
| extend country = tostring(LocationData[0].country)
| extend state = tostring(LocationData[0].state)
| extend isp = tostring(LocationData[0].isp)
| extend org = tostring(LocationData[0].org)
| extend query = tostring(query)
| extend timestamp = toscalar(SigninLogs | where UserPrincipalName == user | summarize max(TimeGenerated))
| project timestampuseripcitystatecountryisporgquery
| order by timestamp desc
| project-away timestamp
| summarize make_list((userquery)) by bin(timestampquery_frequency)
| extend list = todynamic(list_[0])
| project TimeGenerated = timestampUserPrincipalName = list[0]CallerIPAddress = list[1]City = list[2]State = list[3]Country = list[4]ISP = list[5]Organization = list[6]Query = list[7]
| extend timestamp = todatetime(timestamp)
| where timestamp between (ago(query_period)..now())
| project TimeGeneratedUserPrincipalNameOrganizationQuery
| order by TimeGenerated desc
| limit 10000