Detection rules › Kusto
Attempt to bypass conditional access rule in Microsoft Entra ID
'Identifies an attempt to Bypass conditional access rule(s) in Microsoft Entra ID. The ConditionalAccessStatus column value details if there was an attempt to bypass Conditional Access or if the Conditional access rule was not satisfied (ConditionalAccessStatus == 1). References: https://docs.microsoft.com/azure/active-directory/conditional-access/overview https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes ConditionalAccessStatus == 0 // Success ConditionalAccessStatus == 1 // Failure ConditionalAccessStatus == 2 // Not Applied ConditionalAccessStatus == 3 // unknown Note: This analytic rule is designed to identify potential attempts to bypass Conditional Access policies. Depending on your organization's configuration, some Conditional Access failures may be legitimate and expected. It is recommended to review alerts generated by this rule in the context of your organization's Conditional Access policies and adjust the detection threshold as needed to reduce false positives. This rule includes an optional filter that leverages a watchlist (ConditionalAccessBenignStatusCodes) to exclude known benign status codes. You can customize this watchlist with status codes that are expected in your environment and do not indicate malicious activity. By default, the watchlist-based filtering is commented out to provide full visibility into all relevant events. You may choose to enable this filter by uncommenting the corresponding line in the query to reduce noise from known benign scenarios.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078 Valid Accounts |
| Persistence | T1078 Valid Accounts, T1098 Account Manipulation |
Rule body kusto
id: 3af9285d-bb98-4a35-ad29-5ea39ba0c628
name: Attempt to bypass conditional access rule in Microsoft Entra ID
description: |
'Identifies an attempt to Bypass conditional access rule(s) in Microsoft Entra ID.
The ConditionalAccessStatus column value details if there was an attempt to bypass Conditional Access or if the Conditional access rule was not satisfied (ConditionalAccessStatus == 1).
References:
https://docs.microsoft.com/azure/active-directory/conditional-access/overview
https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins
https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes
ConditionalAccessStatus == 0 // Success
ConditionalAccessStatus == 1 // Failure
ConditionalAccessStatus == 2 // Not Applied
ConditionalAccessStatus == 3 // unknown
Note:
This analytic rule is designed to identify potential attempts to bypass Conditional Access policies. Depending on your organization's configuration, some Conditional Access failures may be legitimate and expected.
It is recommended to review alerts generated by this rule in the context of your organization's Conditional Access policies and adjust the detection threshold as needed to reduce false positives.
This rule includes an optional filter that leverages a watchlist (ConditionalAccessBenignStatusCodes) to exclude known benign status codes. You can customize this watchlist with status codes that are expected in your environment and do not indicate malicious activity.
By default, the watchlist-based filtering is commented out to provide full visibility into all relevant events. You may choose to enable this filter by uncommenting the corresponding line in the query to reduce noise from known benign scenarios.'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1078
- T1098
query: |
let threshold = 1; // Modify this threshold value to reduce false positives based on your environment
let aadFunc = (tableName:string){
table(tableName)
| where ConditionalAccessStatus == 1 or ConditionalAccessStatus =~ "failure"
| mv-apply CAP = parse_json(ConditionalAccessPolicies) on (
project ConditionalAccessPoliciesName = CAP.displayName, result = CAP.result
| where result =~ "failure"
)
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
| extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
// Filter out status codes using watchlist
// uncomment the following line to enable watchlist filtering
// | join kind=leftanti (_GetWatchlist("ConditionalAccessBenignStatusCodes") | project StatusCode) on StatusCode
| extend Status = strcat(StatusCode, ": ", ResultDescription)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Status = make_list(Status,10), StatusDetails = make_list(StatusDetails,50), IPAddresses = make_list(IPAddress,100), IPAddressCount = dcount(IPAddress), CorrelationIds = make_list(CorrelationId,100), ConditionalAccessPoliciesName = make_list(ConditionalAccessPoliciesName,100)
by UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, Type
| where IPAddressCount > threshold and StatusDetails !has "MFA successfully completed"
| mv-expand IPAddresses, Status, StatusDetails, CorrelationIds
| extend Status = strcat(Status, " ", StatusDetails)
| summarize IPAddresses = make_set(IPAddresses,100), Status = make_set(Status,10), CorrelationIds = make_set(CorrelationIds,100), ConditionalAccessPoliciesName = make_set(ConditionalAccessPoliciesName,100)
by StartTime, EndTime, UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, IPAddressCount, Type
| extend IPAddressFirst = tostring(IPAddresses[0]), Name = tostring(split(UserPrincipalName, "@")[0]), UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: UserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddressFirst
version: 1.0.8
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 1;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let binding: aadFunc
let aadFunc = (tableName:string){
table(tableName)
| where ConditionalAccessStatus == 1 or ConditionalAccessStatus =~ "failure"
| mv-apply CAP = parse_json(ConditionalAccessPolicies) on (
project ConditionalAccessPoliciesName = CAP.displayName, result = CAP.result
| where result =~ "failure"
)
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
| extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
| extend Status = strcat(StatusCode, ": ", ResultDescription)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Status = make_list(Status,10), StatusDetails = make_list(StatusDetails,50), IPAddresses = make_list(IPAddress,100), IPAddressCount = dcount(IPAddress), CorrelationIds = make_list(CorrelationId,100), ConditionalAccessPoliciesName = make_list(ConditionalAccessPoliciesName,100)
by UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, Type
| where IPAddressCount > threshold and StatusDetails !has "MFA successfully completed"
| mv-expand IPAddresses, Status, StatusDetails, CorrelationIds
| extend Status = strcat(Status, " ", StatusDetails)
| summarize IPAddresses = make_set(IPAddresses,100), Status = make_set(Status,10), CorrelationIds = make_set(CorrelationIds,100), ConditionalAccessPoliciesName = make_set(ConditionalAccessPoliciesName,100)
by StartTime, EndTime, UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, IPAddressCount, Type
| extend IPAddressFirst = tostring(IPAddresses[0]), Name = tostring(split(UserPrincipalName, "@")[0]), UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};
Derived from threshold.
union isfuzzy=true (2 sources)
Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt