Detection rules › Kusto
Detect suspicious conditional access policy modifications
This detection rule flags events where conditional access policies are getting weaker, when modifications to CA inclusion or exclusion groups are happening, or when the effectiveness of a policy is disabled.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1556.009 Modify Authentication Process: Conditional Access Policies |
| Defense Impairment | T1556.009 Modify Authentication Process: Conditional Access Policies |
| Credential Access | T1556.009 Modify Authentication Process: Conditional Access Policies |
References
Event coverage
Rule body yaml
// !! TO DO: CHANGE TO YOUR CA GROUP NAMING CONVENTION !!
let ca_include_naming_convention = "CA-Include";
let ca_exclude_naming_convention = "CA-Exclude";
// OPTIONAL - Get PIM activations with justifications for CA changes
let ca_pim_activations = AuditLogs
// Get PIM activations
| where TimeGenerated > ago(24h)
| where OperationName contains "completed (PIM activation)"
// Parse details
| parse AdditionalDetails with * "{\"key\":\"StartTime\",\"value\":\"" PimStartTime "\"" * "{\"key\":\"ExpirationTime\",\"value\":\"" PimExpirationTime "\"" * "{\"key\":\"Justification\",\"value\":\"" PimJustification "\"" *
// Only get CA related PIM justifications
| where PimJustification has_any ("Conditional Access", "CA", "Trusted", "Named", "Location")
// Extend and projects
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| project OperationName, PimJustification, PimStartTime, PimExpirationTime, UserPrincipalName;
// Get suspicious policy changes
let policy_changes = AuditLogs
// Get CA updates
| where TimeGenerated > ago(24h)
| where OperationName in ("Update conditional access policy", "Delete conditional access policy")
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Save the new and old values
| extend NewValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).conditions
| extend OldValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).conditions
| extend NewValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).grantControls
| extend OldValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).grantControls
| extend NewValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).sessionControls
| extend OldValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).sessionControls
| extend NewState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).state
| extend OldState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).state
// Count the new inlude arrays
| extend CountNewUserIncludes = array_length(NewValueConditions.users.includeUsers),
CountNewRoleIncludes = array_length(NewValueConditions.users.includeRoles),
CountNewGroupIncludes = array_length(NewValueConditions.users.includeGroups),
CountNewUserActionIncludes = array_length(NewValueConditions.applications.inlcudeUserActions),
CountNewAuthContextIncludes = array_length(NewValueConditions.applications.includeAuthenticationContextClassReferences),
CountNewApplicationIncludes = array_length(NewValueConditions.applications.inlcudeApplications),
CountNewLocationIncludes = array_length(NewValueConditions.locations.includeLocations),
CountNewPlatformIncludes = array_length(NewValueConditions.platforms.includePlatforms)
// Count the old inlude arrays
| extend CountOldUserIncludes = array_length(OldValueConditions.users.includeUsers),
CountOldRoleIncludes = array_length(OldValueConditions.users.includeRoles),
CountOldGroupIncludes = array_length(OldValueConditions.users.includeGroups),
CountOldUserActionIncludes = array_length(OldValueConditions.applications.inlcudeUserActions),
CountOldAuthContextIncludes = array_length(OldValueConditions.applications.includeAuthenticationContextClassReferences),
CountOldApplicationIncludes = array_length(OldValueConditions.applications.inlcudeApplications),
CountOldLocationIncludes = array_length(OldValueConditions.locations.includeLocations),
CountOldPlatformIncludes = array_length(OldValueConditions.platforms.includePlatforms)
// Count the new exclude arrays
| extend CountNewUserExcludes = array_length(NewValueConditions.users.excludeUsers),
CountNewRoleExcludes = array_length(NewValueConditions.users.excludeRoles),
CountNewGroupExcludes = array_length(NewValueConditions.users.excludeGroups),
CountNewApplicationExcludes = array_length(NewValueConditions.applications.excludeApplications),
CountNewLocationExcludes = array_length(NewValueConditions.locations.excludeLocations),
CountNewPlatformExcludes = array_length(NewValueConditions.platforms.excludePlatforms)
// Count the old exclude arrays
| extend CountOldUserExcludes = array_length(OldValueConditions.users.excludeUsers),
CountOldRoleExcludes = array_length(OldValueConditions.users.excludeRoles),
CountOldGroupExcludes = array_length(OldValueConditions.users.excludeGroups),
CountOldApplicationExcludes = array_length(OldValueConditions.applications.excludeApplications),
CountOldLocationExcludes = array_length(OldValueConditions.locations.excludeLocations),
CountOldPlatformExcludes = array_length(OldValueConditions.platforms.excludePlatforms)
// Alert when includes are taken away and excludes are added, application filter changes, or AppType changes
| extend Reasons = dynamic([])
| extend Reasons = iff(CountNewUserIncludes < CountOldUserIncludes, array_concat(Reasons, dynamic(["User removed from include"])), Reasons)
| extend Reasons = iff(CountNewRoleIncludes < CountOldRoleIncludes, array_concat(Reasons, dynamic(["Role removed from include"])), Reasons)
| extend Reasons = iff(CountNewGroupIncludes < CountOldGroupIncludes, array_concat(Reasons, dynamic(["Group removed from include"])), Reasons)
| extend Reasons = iff(CountNewUserExcludes > CountOldUserExcludes, array_concat(Reasons, dynamic(["User added to exclude"])), Reasons)
| extend Reasons = iff(CountNewRoleExcludes > CountOldRoleExcludes, array_concat(Reasons, dynamic(["Role added to exclude"])), Reasons)
| extend Reasons = iff(CountNewGroupExcludes > CountOldGroupExcludes, array_concat(Reasons, dynamic(["Group added to exclude"])), Reasons)
| extend Reasons = iff(CountNewUserActionIncludes < CountOldUserActionIncludes, array_concat(Reasons, dynamic(["User action removed from include"])), Reasons)
| extend Reasons = iff(CountNewAuthContextIncludes < CountOldAuthContextIncludes, array_concat(Reasons, dynamic(["Authentication context removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationIncludes < CountOldApplicationIncludes, array_concat(Reasons, dynamic(["Application removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationExcludes > CountOldApplicationExcludes, array_concat(Reasons, dynamic(["Application added to exclude"])), Reasons)
| extend Reasons = iff(CountNewLocationIncludes < CountOldLocationIncludes, array_concat(Reasons, dynamic(["Locations removed from include"])), Reasons)
| extend Reasons = iff(CountNewLocationExcludes > CountOldLocationExcludes, array_concat(Reasons, dynamic(["Locations added to exclude"])), Reasons)
| extend Reasons = iff(CountNewPlatformIncludes < CountOldPlatformIncludes, array_concat(Reasons, dynamic(["Platforms removed from include"])), Reasons)
| extend Reasons = iff(CountNewPlatformExcludes > CountOldPlatformExcludes, array_concat(Reasons, dynamic(["Platforms added to exclude"])), Reasons)
// Flag general changes
| extend Reasons = iff(tostring(NewValueConditions.applications.applicationFilter) != tostring(OldValueConditions.applications.applicationFilter), array_concat(Reasons, dynamic(["Application filter changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.clientAppTypes) != tostring(OldValueConditions.clientAppTypes), array_concat(Reasons, dynamic(["Client app type changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.userRiskLevels) != tostring(OldValueConditions.userRiskLevels), array_concat(Reasons, dynamic(["User risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.signInRiskLevels) != tostring(OldValueConditions.signInRiskLevels), array_concat(Reasons, dynamic(["Sign-in risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.servicePrincipalRiskLevels) != tostring(OldValueConditions.servicePrincipalRiskLevels), array_concat(Reasons, dynamic(["Service Principal risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueGrandControls) != tostring(OldValueGrandControls), array_concat(Reasons, dynamic(["Grant controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueSessionControls) != tostring(OldValueSessionControls), array_concat(Reasons, dynamic(["Session controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.devices) != tostring(OldValueConditions.devices), array_concat(Reasons, dynamic(["Device conditions changed"])), Reasons)
// Flag Change from include 'all' to only include specifics (since this can evade the count detections)
| extend Reasons = iff(tostring(OldValueConditions.locations.includeLocations) contains "all" and tostring(NewValueConditions.locations.includeLocations) !contains "all", array_concat(Reasons, dynamic(["Include locations changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.platforms.includePlatforms) contains "all" and tostring(NewValueConditions.platforms.includePlatforms) !contains "all", array_concat(Reasons, dynamic(["Include platforms changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.users.includeUsers) contains "all" and tostring(NewValueConditions.users.includeUsers) !contains "all", array_concat(Reasons, dynamic(["Include users changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.applications.includeApplications) contains "all" and tostring(NewValueConditions.applications.includeApplications) !contains "all", array_concat(Reasons, dynamic(["Include applications changed from all to specific"])), Reasons)
// Flag state change to inactive
| extend Reasons = iff(tostring(OldState) == "enabled" and tostring(NewState) != "enabled", array_concat(Reasons, dynamic(["Policy was disabled"])), Reasons)
// Flag policy deletion
| extend Reasons = iff(OperationName == "Delete conditional access policy", array_concat(Reasons, dynamic(["Policy was deleted"])), Reasons);
// Get trusted named location changes
let named_locations = AuditLogs
// Get named location changes
| where TimeGenerated > ago(24h)
| where OperationName in ("Add named location", "Update named location")
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Always flag when the named location is trusted
| extend NewValueIsTrusted = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).isTrusted
| where NewValueIsTrusted == "true"
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = iff(OperationName == "Add named location", array_concat(Reasons, dynamic(["Trusted named location was added"])), Reasons)
| extend Reasons = iff(OperationName == "Update named location", array_concat(Reasons, dynamic(["Trusted named location was updated"])), Reasons);
// Get changes to groups used in CA policies
let remove_from_include_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Remove member from group"
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Search for the display name of the edited group and find groups with CA naming convention
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_include_naming_convention
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member removed from include group used in CA policy"]);
let add_to_exclude_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to group"
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Search for the display name of the edited group and find groups with CA naming convention
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_exclude_naming_convention
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member added to exclude group used in CA policy"]);
// Union all detections
union policy_changes, named_locations, remove_from_include_group, add_to_exclude_group
// Check if reason array is empty
| where Reasons != "[]"
// Sorting and project
| sort by TimeGenerated desc
| project TimeGenerated, OperationName, InitiatedBy, LoggedByService, Result, TargetResources, AADOperationType, Reasons
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
// Look for PIM activations from the same user who performed changes
| join kind=leftouter ca_pim_activations on UserPrincipalName
| project-away UserPrincipalName1
// Check if PIM was justified for user, and only show non-justified PIMs
| extend JustifiedPIM = iff(isnotempty(PimStartTime) and isnotempty(PimExpirationTime) and TimeGenerated between (todatetime(PimStartTime) .. todatetime(PimExpirationTime)), true, false)
| where JustifiedPIM == false
Stages and Predicates
Parameters
let ca_include_naming_convention = "CA-Include";
let ca_exclude_naming_convention = "CA-Exclude";
Let binding: ca_pim_activations
let ca_pim_activations = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName contains "completed (PIM activation)"
| parse AdditionalDetails with * "{\"key\":\"StartTime\",\"value\":\"" PimStartTime "\"" * "{\"key\":\"ExpirationTime\",\"value\":\"" PimExpirationTime "\"" * "{\"key\":\"Justification\",\"value\":\"" PimJustification "\"" *
| where PimJustification has_any ("Conditional Access", "CA", "Trusted", "Named", "Location")
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| project OperationName, PimJustification, PimStartTime, PimExpirationTime, UserPrincipalName;
Let binding: policy_changes
let policy_changes = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in ("Update conditional access policy", "Delete conditional access policy")
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| extend NewValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).conditions
| extend OldValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).conditions
| extend NewValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).grantControls
| extend OldValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).grantControls
| extend NewValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).sessionControls
| extend OldValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).sessionControls
| extend NewState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).state
| extend OldState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).state
| extend CountNewUserIncludes = array_length(NewValueConditions.users.includeUsers),
CountNewRoleIncludes = array_length(NewValueConditions.users.includeRoles),
CountNewGroupIncludes = array_length(NewValueConditions.users.includeGroups),
CountNewUserActionIncludes = array_length(NewValueConditions.applications.inlcudeUserActions),
CountNewAuthContextIncludes = array_length(NewValueConditions.applications.includeAuthenticationContextClassReferences),
CountNewApplicationIncludes = array_length(NewValueConditions.applications.inlcudeApplications),
CountNewLocationIncludes = array_length(NewValueConditions.locations.includeLocations),
CountNewPlatformIncludes = array_length(NewValueConditions.platforms.includePlatforms)
| extend CountOldUserIncludes = array_length(OldValueConditions.users.includeUsers),
CountOldRoleIncludes = array_length(OldValueConditions.users.includeRoles),
CountOldGroupIncludes = array_length(OldValueConditions.users.includeGroups),
CountOldUserActionIncludes = array_length(OldValueConditions.applications.inlcudeUserActions),
CountOldAuthContextIncludes = array_length(OldValueConditions.applications.includeAuthenticationContextClassReferences),
CountOldApplicationIncludes = array_length(OldValueConditions.applications.inlcudeApplications),
CountOldLocationIncludes = array_length(OldValueConditions.locations.includeLocations),
CountOldPlatformIncludes = array_length(OldValueConditions.platforms.includePlatforms)
| extend CountNewUserExcludes = array_length(NewValueConditions.users.excludeUsers),
CountNewRoleExcludes = array_length(NewValueConditions.users.excludeRoles),
CountNewGroupExcludes = array_length(NewValueConditions.users.excludeGroups),
CountNewApplicationExcludes = array_length(NewValueConditions.applications.excludeApplications),
CountNewLocationExcludes = array_length(NewValueConditions.locations.excludeLocations),
CountNewPlatformExcludes = array_length(NewValueConditions.platforms.excludePlatforms)
| extend CountOldUserExcludes = array_length(OldValueConditions.users.excludeUsers),
CountOldRoleExcludes = array_length(OldValueConditions.users.excludeRoles),
CountOldGroupExcludes = array_length(OldValueConditions.users.excludeGroups),
CountOldApplicationExcludes = array_length(OldValueConditions.applications.excludeApplications),
CountOldLocationExcludes = array_length(OldValueConditions.locations.excludeLocations),
CountOldPlatformExcludes = array_length(OldValueConditions.platforms.excludePlatforms)
| extend Reasons = dynamic([])
| extend Reasons = iff(CountNewUserIncludes < CountOldUserIncludes, array_concat(Reasons, dynamic(["User removed from include"])), Reasons)
| extend Reasons = iff(CountNewRoleIncludes < CountOldRoleIncludes, array_concat(Reasons, dynamic(["Role removed from include"])), Reasons)
| extend Reasons = iff(CountNewGroupIncludes < CountOldGroupIncludes, array_concat(Reasons, dynamic(["Group removed from include"])), Reasons)
| extend Reasons = iff(CountNewUserExcludes > CountOldUserExcludes, array_concat(Reasons, dynamic(["User added to exclude"])), Reasons)
| extend Reasons = iff(CountNewRoleExcludes > CountOldRoleExcludes, array_concat(Reasons, dynamic(["Role added to exclude"])), Reasons)
| extend Reasons = iff(CountNewGroupExcludes > CountOldGroupExcludes, array_concat(Reasons, dynamic(["Group added to exclude"])), Reasons)
| extend Reasons = iff(CountNewUserActionIncludes < CountOldUserActionIncludes, array_concat(Reasons, dynamic(["User action removed from include"])), Reasons)
| extend Reasons = iff(CountNewAuthContextIncludes < CountOldAuthContextIncludes, array_concat(Reasons, dynamic(["Authentication context removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationIncludes < CountOldApplicationIncludes, array_concat(Reasons, dynamic(["Application removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationExcludes > CountOldApplicationExcludes, array_concat(Reasons, dynamic(["Application added to exclude"])), Reasons)
| extend Reasons = iff(CountNewLocationIncludes < CountOldLocationIncludes, array_concat(Reasons, dynamic(["Locations removed from include"])), Reasons)
| extend Reasons = iff(CountNewLocationExcludes > CountOldLocationExcludes, array_concat(Reasons, dynamic(["Locations added to exclude"])), Reasons)
| extend Reasons = iff(CountNewPlatformIncludes < CountOldPlatformIncludes, array_concat(Reasons, dynamic(["Platforms removed from include"])), Reasons)
| extend Reasons = iff(CountNewPlatformExcludes > CountOldPlatformExcludes, array_concat(Reasons, dynamic(["Platforms added to exclude"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.applications.applicationFilter) != tostring(OldValueConditions.applications.applicationFilter), array_concat(Reasons, dynamic(["Application filter changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.clientAppTypes) != tostring(OldValueConditions.clientAppTypes), array_concat(Reasons, dynamic(["Client app type changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.userRiskLevels) != tostring(OldValueConditions.userRiskLevels), array_concat(Reasons, dynamic(["User risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.signInRiskLevels) != tostring(OldValueConditions.signInRiskLevels), array_concat(Reasons, dynamic(["Sign-in risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.servicePrincipalRiskLevels) != tostring(OldValueConditions.servicePrincipalRiskLevels), array_concat(Reasons, dynamic(["Service Principal risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueGrandControls) != tostring(OldValueGrandControls), array_concat(Reasons, dynamic(["Grant controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueSessionControls) != tostring(OldValueSessionControls), array_concat(Reasons, dynamic(["Session controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.devices) != tostring(OldValueConditions.devices), array_concat(Reasons, dynamic(["Device conditions changed"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.locations.includeLocations) contains "all" and tostring(NewValueConditions.locations.includeLocations) !contains "all", array_concat(Reasons, dynamic(["Include locations changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.platforms.includePlatforms) contains "all" and tostring(NewValueConditions.platforms.includePlatforms) !contains "all", array_concat(Reasons, dynamic(["Include platforms changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.users.includeUsers) contains "all" and tostring(NewValueConditions.users.includeUsers) !contains "all", array_concat(Reasons, dynamic(["Include users changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.applications.includeApplications) contains "all" and tostring(NewValueConditions.applications.includeApplications) !contains "all", array_concat(Reasons, dynamic(["Include applications changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldState) == "enabled" and tostring(NewState) != "enabled", array_concat(Reasons, dynamic(["Policy was disabled"])), Reasons)
| extend Reasons = iff(OperationName == "Delete conditional access policy", array_concat(Reasons, dynamic(["Policy was deleted"])), Reasons);
Let binding: named_locations
let named_locations = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in ("Add named location", "Update named location")
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| extend NewValueIsTrusted = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).isTrusted
| where NewValueIsTrusted == "true"
| extend Reasons = dynamic([])
| extend Reasons = iff(OperationName == "Add named location", array_concat(Reasons, dynamic(["Trusted named location was added"])), Reasons)
| extend Reasons = iff(OperationName == "Update named location", array_concat(Reasons, dynamic(["Trusted named location was updated"])), Reasons);
Let binding: remove_from_include_group
let remove_from_include_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Remove member from group"
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_include_naming_convention
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member removed from include group used in CA policy"]);
Derived from ca_include_naming_convention.
Let binding: add_to_exclude_group
let add_to_exclude_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to group"
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_exclude_naming_convention
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member added to exclude group used in CA policy"]);
Derived from ca_exclude_naming_convention.
union (4 sources)
Each leg below queries one source; the rule matches if any leg does. Sources: policy_changes, named_locations, remove_from_include_group, add_to_exclude_group
Leg 1: policy_changes
Leg 2: named_locations
Leg 3: remove_from_include_group
Leg 4: add_to_exclude_group
Applied to the combined result
| where Reasons != "[]" | sort by TimeGenerated desc | project TimeGenerated, OperationName, InitiatedBy, LoggedByService, Result, TargetResources, AADOperationType, Reasons | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) | join kind=leftouter ca_pim_activations on UserPrincipalName | project-away UserPrincipalName1 | extend JustifiedPIM = iff(isnotempty(PimStartTime) and isnotempty(PimExpirationTime) and TimeGenerated between (todatetime(PimStartTime) .. todatetime(PimExpirationTime)), true, false) | where JustifiedPIM == false
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
JustifiedPIM | eq |
|
NewValueIsTrusted | eq |
|
OperationName | contains |
|
OperationName | eq |
|
OperationName | in |
|
PimJustification | match |
|
Reasons | ne |
|
TargetResources_modifiedProperties | contains |
|
displayName | eq |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AADOperationType | project |
InitiatedBy | project |
LoggedByService | project |
OperationName | project |
Reasons | project |
Result | project |
TargetResources | project |
TimeGenerated | project |
UserPrincipalName | extend |
JustifiedPIM | extend |