Skip to content

Noisy analytic rule - Brute force attack against Azure Portal - template version 2.1.4 #12567

@MikeP324

Description

@MikeP324

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:

  1. Create 100+ CA policies.
  2. Use a test user account with MFA configured with access to Azure portal.
  3. 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])

bruteforce-azureportal-events.zip

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions