List sensitive group membership changes, including who was added or removed to what group, and who made the change.
Prerequisites for using this KQL query
The Microsoft Defender for Identity Domain controller Sensor needs to be installed on your domain controllers. Perform query using Microsoft Defender > Hunting > Advanced hunting.
KQL query to get Active Directory sensitive group membership changes
let SensitiveGroupName = pack_array( // Declare Sensitive Group names. Add any groups that you manually tagged as sensitive
'Account Operators',
'Administrators',
'Domain Admins',
'Backup Operators',
'Domain Controllers',
'Enterprise Admins',
'Enterprise Read-only Domain Controllers',
'Group Policy Creator Owners',
'Incoming Forest Trust Builders',
'Microsoft Exchange Servers',
'Network Configuration Operators',
'Microsoft Exchange Servers',
'Print Operators',
'Read-only Domain Controllers',
'Replicator',
'Schema Admins',
'Server Operators'
);
IdentityDirectoryEvents
| where Application == "Active Directory"
| where ActionType == "Group Membership changed"
| where DestinationDeviceName != "" // Exclude activites coming AD Sync changes.
| extend ToGroup = tostring(parse_json(AdditionalFields).["TO.GROUP"]) // Extracts the group name if action is add enity to a group.
| extend FromGroup = tostring(parse_json(AdditionalFields).["FROM.GROUP"]) // extracts the group name if action is remove entity from a group.
| extend Action = iff(isempty(ToGroup), "Remove", "Add") // calculates if the action is Remove or Add
| extend GroupModified = iff(isempty(ToGroup), FromGroup, ToGroup) // group name that the action was taken on
| extend Target_Group = tostring(parse_json(AdditionalFields).["TARGET_OBJECT.GROUP"])
| where GroupModified in~ (SensitiveGroupName)
| project Timestamp, Action, GroupModified, Target_Account = TargetAccountDisplayName, Target_UPN = TargetAccountUpn, Target_Group, DC=DestinationDeviceName, Actor=AccountName, ActorDomain=AccountDomain, AdditionalFields
| sort by Timestamp desc
// credit: Gerson Levitz (techcommunity.microsoft.com)
This will return a result set with columns Timestamp
, Action
, GroupModified
, Target_Account
, Target_UPN
, Target Group
, DC
, Actor
, ActorDomain
, and an array of AdditionalFields
.
Deconstructing the KQL query
Read on to understand HOW the query above works.
The goal of this query is to identify changes in group membership within Active Directory, specifically focusing on sensitive groups. We want to track when users are added to or removed from these critical groups.
High-level summary
- Declare sensitive group names: We start by listing the names of sensitive groups we’re interested in.
- Filter relevant events: We filter events related to group membership changes in Active Directory.
- Extract group information: We extract details about the groups involved in the change (added or removed).
- Determine action type: We determine whether the action was an addition or removal.
- Select relevant fields: We choose specific fields to display in the final result.
- Sort by timestamp: We arrange the results in chronological order.
let SensitiveGroupName = pack_array( // Declare Sensitive Group names. Add any groups that you manually tagged as sensitive
'Account Operators',
'Administrators',
'Domain Admins',
'Backup Operators',
'Domain Controllers',
'Enterprise Admins',
'Enterprise Read-only Domain Controllers',
'Group Policy Creator Owners',
'Incoming Forest Trust Builders',
'Microsoft Exchange Servers',
'Network Configuration Operators',
'Microsoft Exchange Servers',
'Print Operators',
'Read-only Domain Controllers',
'Replicator',
'Schema Admins',
'Server Operators'
);
We create a list called SensitiveGroupName
containing the names of sensitive groups. These are standard sensitive groups, can add/remove groups to the list as needed.
IdentityDirectoryEvents
This is the data table that contains events related to identity directories.
| where Application == "Active Directory"
Filters the events to include only those related to “Active Directory”.
| where ActionType == "Group Membership changed"
Further narrows down the events to those where the action type indicates a change in group membership.
| where DestinationDeviceName != ""
Excludes events that originate from AD Sync changes by ensuring the DestinationDeviceName
field is not empty.
| extend ToGroup = tostring(parse_json(AdditionalFields).["TO.GROUP"])
Creates a new field called ToGroup
by extracting the group name to which an entity was added, if applicable.
| extend FromGroup = tostring(parse_json(AdditionalFields).["FROM.GROUP"])
Similarly, creates a new field called FromGroup
by extracting the group name from which an entity was removed, if applicable.
| extend Action = iff(isempty(ToGroup), "Remove", "Add")
Determines the action taken—either “Remove” or “Add”—based on whether the ToGroup
field is empty.
| extend GroupModified = iff(isempty(ToGroup), FromGroup, ToGroup)
Identifies the group that was modified by the action, using FromGroup
if ToGroup
is empty, and vice versa.
| extend Target_Group = tostring(parse_json(AdditionalFields).["TARGET_OBJECT.GROUP"])
Extracts the target group affected by the action.
| where GroupModified in~ (SensitiveGroupName)
The in~
operator checks whether the case-insensitive value in the GroupModified
column exists in a specified set or list, in this case SensitiveGroupName
.
| project Timestamp, Action, GroupModified, Target_Account = TargetAccountDisplayName, Target_UPN = TargetAccountUpn, Target_Group, DC=DestinationDeviceName, Actor=AccountName, ActorDomain=AccountDomain, AdditionalFields
Selects and renames the fields to be displayed in the final output.
| sort by Timestamp desc
Sorts the results in descending order by the Timestamp
, showing the most recent events first.
Reference
- Log Analytics table: IdentityDirectoryEvents | learn.microsoft.com
- Kusto: iff() | learn.microsoft.com
- Kusto: in~ | learn.microsoft.com
- Protected groups in Active Directory | learn.microsoft.com
- Track changes to sensitive groups with Advanced Hunting in Microsoft 365 Defender | techcommunity.microsoft.com