-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Describe the bug
The templated analytic rule "Brute force attack against Azure Portal" templated version 2.1.4 is a little noisy due to the multiple events with the same TimeGenerated timestamp. The only differences is the field ConditionalAccessPolicies which reports the users status against those CA policies.
As an org increases the number of CA policies the number of events will increase as the limit for reporting data on the field is 34 policies. In our case we have 100+ policies which means we get four events
To Reproduce
Steps to reproduce the behavior:
- Create 100+ CA policies.
- Use a test user account with MFA configured with access to Azure portal.
- Ensure MFA authentication is triggered and review the number of events reported in the SigninLogs table.
Expected behavior
You can attempt to modify the thresholds to address the multiple events or how about adding the following code
| project-away ConditionalAccessPolicies'
| summarize arg_max(TimeGenerated, *) by TimeGenerated
Screenshots
See zip file.
If applicable, add screenshots to help explain your problem.
Additional context
Existing KQL code we are using.
// Set threshold value for deviation
let threshold = 25;
// Set the time range for the query
let timeRange = 24h;
// Set the authentication window duration
let authenticationWindow = 20m;
// Define a reusable function 'aadFunc' that takes a table name as input
let aadFunc = (tableName: string) {
// Query the specified table
table(tableName)
// Filter data within the last 24 hours
| where TimeGenerated > ago(1d)
// Filter records related to "Azure Portal" applications
| where AppDisplayName has "Azure Portal"
// Extract and transform some fields
| extend
DeviceDetail = todynamic(DeviceDetail),
LocationDetails = todynamic(LocationDetails)
| extend
OS = tostring(DeviceDetail.operatingSystem),
Browser = tostring(DeviceDetail.browser),
State = tostring(LocationDetails.state),
City = tostring(LocationDetails.city),
Region = tostring(LocationDetails.countryOrRegion)
// Categorize records as Success or Failure based on ResultType
| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
// Sort and identify sessions
| sort by UserPrincipalName asc, TimeGenerated asc
| extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
// Summarize data
| summarize FailureOrSuccessCount = count() by FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
| summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
// Filter records where "Success" occurs in the middle of a session
| where array_index_of(list_FailureOrSuccess, "Success") != 0
| where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
// Remove unnecessary columns from the output
| project-away SessionStartedUtc, list_FailureOrSuccess
// Join with another table and calculate deviation
| join kind=inner (
table(tableName)
| where TimeGenerated > ago(7d)
| where AppDisplayName has "Azure Portal"
| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
| summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
) on UserPrincipalName
| extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
// Filter records based on deviation and failure count criteria
| where Deviation > threshold and FailureCountBeforeSuccess >= 25
// Expand the IPAddress array
| mv-expand IPAddress
| extend IPAddress = tostring(IPAddress)
| extend timestamp = StartTime
};
// Call 'aadFunc' with different table names and union the results
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
// Additional transformation - Split UserPrincipalName
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])