Attackers with privileged access to an Entra ID tenant may disable or delete Conditional Access policies to bypass security controls and maintain persistence, making proactive hunting in Azure Sentinel critical to detect and respond to potential lateral movement and privilege escalation attempts. This behavior is indicative of adversarial activity aimed at undermining organizational security postures, which aligns with MITRE techniques T1562.001 and T1556.
KQL Query
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
// Deleted policies - always surface regardless of modifiedProperties content
let Deletions =
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where Category =~ "Policy"
| where OperationName =~ "Delete conditional access policy"
| where Result =~ "success"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend PolicyId = tostring(TargetResources[0].id)
| extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend ActorIp = iff(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)),
tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
tostring(parse_json(tostring(InitiatedBy.app)).ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend StateChange = "Deleted"
| extend OldValue = ""
| extend NewValue = "";
// Updated policies where the state transitioned from enabled to disabled
let Disablements =
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where Category =~ "Policy"
| where OperationName =~ "Update conditional access policy"
| where Result =~ "success"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend PolicyId = tostring(TargetResources[0].id)
| extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend ActorIp = iff(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)),
tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
tostring(parse_json(tostring(InitiatedBy.app)).ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| mv-expand ModProp = TargetResources[0].modifiedProperties
| extend PropName = tostring(ModProp.displayName)
| extend OldValue = tostring(ModProp.oldValue)
| extend NewValue = tostring(ModProp.newValue)
| where PropName =~ "State"
and OldValue has "enabled"
and NewValue has "disabled"
| extend StateChange = "Disabled";
union Deletions, Disablements
| extend AccountName = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
| project
TimeGenerated,
OperationName,
PolicyName,
PolicyId,
StateChange,
OldValue,
NewValue,
Actor,
AccountName,
AccountUPNSuffix,
ActorIp,
CorrelationId
| sort by TimeGenerated desc
id: 0456a783-2fd9-4e07-aa05-4aa0afdab0a6
name: Conditional Access policy disabled or deleted
description: |
Hunting query that identifies Conditional Access policies that have been disabled or
deleted. An attacker who obtains privileged access to an Entra ID tenant will commonly
disable or delete CA policies to remove multi-factor authentication requirements,
trusted location restrictions, or compliant-device conditions before proceeding with
lateral movement or data exfiltration. Disabling a CA policy is a silent, low-noise
action that does not interrupt active sessions and may go unnoticed without dedicated
monitoring.
This query surfaces two event types: policy state changes from enabled to disabled
(OperationName: Update conditional access policy) and outright policy deletions
(OperationName: Delete conditional access policy). Both are extracted from the Policy
category of AuditLogs and include the actor identity and source IP for analyst
correlation.
Analysts must validate every result. Benign matches include authorized policy
lifecycle management, scheduled policy reviews, tenant restructuring activities,
and break-glass account testing. The signal value is highest when the actor is not
a known CA administrator or when the action occurs outside of change-window hours.
References:
- https://learn.microsoft.com/azure/active-directory/conditional-access/overview
- https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities
- https://attack.mitre.org/techniques/T1562/001/
- https://attack.mitre.org/techniques/T1556/
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- DefenseEvasion
- Persistence
relevantTechniques:
- T1562.001
- T1556
query: |
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
// Deleted policies - always surface regardless of modifiedProperties content
let Deletions =
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where Category =~ "Policy"
| where OperationName =~ "Delete conditional access policy"
| where Result =~ "success"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend PolicyId = tostring(TargetResources[0].id)
| extend ActorUpn = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend ActorApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend ActorIp = iff(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)),
tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
tostring(parse_json(tostring(InitiatedBy.app)).ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend StateChange = "Deleted"
| extend OldValue = ""
| extend NewValue = "";
// Updated policies whe
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
Scenario: Scheduled Job to Archive Old Conditional Access Policies
Description: A system administrator runs a scheduled job to archive or delete old Conditional Access policies as part of routine cleanup.
Filter/Exclusion: Exclude policies that are marked as “archived” or have a specific “retention tag” applied in Azure AD. Use a filter like policy_state = 'archived' or policy_id IN (list_of_archived_policy_ids).
Scenario: Admin Task to Disable Policy for Testing Purposes
Description: An admin temporarily disables a Conditional Access policy to test its impact on user access or to simulate a failure scenario.
Filter/Exclusion: Exclude policies that are associated with a test environment or have a “test” tag in their description. Use a filter like policy_description LIKE '%test%' or environment = 'test'.
Scenario: Automated Policy Rotation for Compliance
Description: A compliance tool or script automatically rotates Conditional Access policies by disabling the old one and enabling a new version as part of a compliance update.
Filter/Exclusion: Exclude policies that are part of a known compliance rotation process. Use a filter like policy_rotation_tag = 'true' or policy_id IN (list_of_rotation_policies).
Scenario: User-Driven Policy Disable via Azure AD Admin Center
Description: An admin manually disables a Conditional Access policy through the Azure AD Admin Center during a troubleshooting session or to investigate an access issue.
Filter/Exclusion: Exclude policies that were recently modified by a known admin user. Use a filter like modified_by_user = 'admin_username' or modified_time > (current_time - 1 hour).
Scenario: Conditional Access Policy Disabled During Migration
Description: A Conditional Access policy is disabled temporarily during an identity provider migration or tenant migration process