Reading time: ~14–18 min • Last updated:
Nested groups are convenient, flexible, and dangerously opaque. This guide shows how to audit them properly in Active Directory and Microsoft Entra, with path-aware reporting, Windows event alerts, and Graph transitive queries.
Why nested membership matters now
Definition (snippet-ready): Nested group membership means a group is a member of another group. Effective access flows through the entire chain, not just the immediate parent.
In modern hybrid identity, group topology grew more complex. Cloud apps, on-prem ACLs, Conditional Access, and JIT elevation meet inside your group graph. Heavy nesting broadens the blast radius of a single change and makes least-privilege hard to prove.
First principles that explain the risks
- Access is transitive. A → B → C means A inherits C’s rights.
- It’s a graph, not a list. Group scope, trusts, and sync rules change edge behavior.
- Local change, global blast radius. One add on a DC ripples into dozens of ACLs and SaaS apps.
- Path is evidence. Flattened lists erase “how” a user got access.
The expert comparison: native auditing, directory queries, and specialized tools
Native Windows auditing (event logs)
Enable Audit Security Group Management for authoritative change events such as 4728/4732/4756 on DCs. Great for detection and forensics, but it doesn’t reconstruct the full current transitive set by itself.
Refs: 4728, 4732, 4756, event encyclopedia.
Directory queries (AD PowerShell; Microsoft Graph for Entra)
Get-ADGroupMember -Recursive and Graph /transitiveMembers return flattened sets. Perfect for reporting, but you still need lineage for remediation.
Refs: Get-ADGroupMember, Graph transitiveMembers.
Specialized tools
Purpose-built auditors visualize paths and enforce guardrails on privileged groups. Use them to answer “who is effectively in Domain Admins—and why?” quickly.
Hands-on technical blueprint (copy/paste ready)
1) Turn on authoritative change events (AD)
- On the Domain Controllers GPO, enable: Computer Configuration → Policies → Windows Settings → Security Settings → Advanced Audit Policy Configuration → Audit Policies → Account Management → Audit Security Group Management (Success; consider Failure).
- Monitor event IDs: 4728/4729 (global), 4732/4733 (domain local), 4756/4757 (universal). Treat 4764 (group type/scope changes) as high-signal configuration drift.
- Forward to your SIEM and alert on a watchlist of privileged groups.
2) Export path-aware nested membership (AD)
Use a BFS/DFS traversal that records parent→child edges and reconstructs lineage for each principal. This avoids recursion limits, detects cycles, and captures object types that basic cmdlets may omit.
param(
[Parameter(Mandatory=$true)][string]$GroupSam,
[string[]]$StopAtGroups = @("Domain Admins","Enterprise Admins","Schema Admins") # optional early stop
)
Import-Module ActiveDirectory
$queue = New-Object System.Collections.Queue
$visited = New-Object System.Collections.Generic.HashSet[string]
$edges = New-Object System.Collections.Generic.Dictionary[string, string[]] # child -> parents (multi)
$root = Get-ADGroup -Identity $GroupSam -Properties member
$queue.Enqueue($root.DistinguishedName)
$visited.Add($root.DistinguishedName) | Out-Null
function Get-MembersRaw([string]$dn){
try {
$obj = Get-ADObject -Identity $dn -Properties member,objectClass,samAccountName
return $obj.member
} catch { return @() }
}
while($queue.Count -gt 0){
$current = $queue.Dequeue()
foreach($child in (Get-MembersRaw $current)){
if(-not $edges.ContainsKey($child)){ $edges[$child] = @() }
$edges[$child] += $current
if(-not $visited.Contains($child)){
$visited.Add($child) | Out-Null
$childObj = Get-ADObject -Identity $child -Properties objectClass
if($childObj.objectClass -contains 'group'){
$sam = (Get-ADGroup -Identity $child -Properties samAccountName).samAccountName
if($StopAtGroups -and $StopAtGroups -contains $sam){ continue }
$queue.Enqueue($child)
}
}
}
}
$result = foreach($leaf in $edges.Keys){
$stack = @([pscustomobject]@{ Node=$leaf; Path=@($leaf) })
$leafPaths = @()
while($stack.Count -gt 0){
$frame = $stack[-1]; $stack = $stack[0..($stack.Count-2)]
if($frame.Node -eq $root.DistinguishedName){
$leafPaths += ,($frame.Path[-1..0] -join " -> ")
continue
}
if($edges.ContainsKey($frame.Node)){
foreach($parent in $edges[$frame.Node]){
$stack += [pscustomobject]@{ Node=$parent; Path=$frame.Path + $parent }
}
}
}
$leafObj = Get-ADObject -Identity $leaf -Properties objectClass,samAccountName,displayName
[pscustomobject]@{
SamAccountName = $leafObj.samAccountName
DisplayName = $leafObj.displayName
ObjectClasses = ($leafObj.objectClass -join ',')
Paths = $leafPaths -join ' || '
}
}
$result | Sort-Object SamAccountName | Format-Table -AutoSize
# Tip: Export for evidence
# $result | Export-Csv .\NestedMembership-$($GroupSam).csv -NoTypeInformation -Encoding UTF8
3) Enumerate effective membership of privileged groups (quick sweep)
$targets = "Domain Admins","Enterprise Admins","Schema Admins"
$everyone = foreach($g in $targets){ Get-ADGroupMember -Identity $g -Recursive -ErrorAction Stop }
$everyone |
Where-Object {$_.objectClass -in @('user','computer')} |
Sort-Object Name -Unique |
Select-Object Name, SamAccountName, objectClass
# Then run the path-aware script against each target to explain WHY each principal is in.
4) Microsoft Entra / Microsoft 365 (transitive cloud members)
Use Microsoft Graph for the full nested set, then reconstruct lineage if needed by walking parent memberships.
# Requires Microsoft.Graph module and appropriate scopes
Connect-MgGraph -Scopes "Group.Read.All","User.Read.All"
$group = Get-MgGroup -Filter "displayName eq 'Privileged Role Admins'"
$members = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($group.Id)/transitiveMembers?$select=id,displayName,userPrincipalName,mail,odata.type"
$members.value | ForEach-Object {
[pscustomobject]@{
Type = $_.'@odata.type'; Name = $_.displayName; UPN = $_.userPrincipalName; Mail = $_.mail; Id = $_.id
}
}
Inherent tendencies, pitfalls, and fixes
- Nesting increases entropy. Prefer explicit membership for Tier-0 roles; avoid broad group → admin group edges.
- Point-in-time blind spots. Directory queries show “now.” Pair them with eventing to explain change history.
- Flattening erases accountability. Always retain lineage for audit and remediation.
- Edge cases: contacts may be skipped by basic cmdlets; query raw
member. Watch for cycles; always trackvisited.
Mental models that keep you sane
- Graph thinking: nodes and edges; entry groups and critical sinks.
- Tier-0 perimeter: fence Domain/Enterprise/Schema Admins (and cloud equivalents). No broad nesting inside.
- Time + state = truth: events for “how it changed,” queries for “what it is now.”
- Path as evidence: remediation is obvious when you can print the chain.
Expert essentials checklist
- Enable Audit Security Group Management on DCs; ingest 4728/4732/4756 (+ 4764 for scope/type changes).
- Maintain a watchlist of Tier-0 groups and alert on any membership change.
- Export path-aware nested membership—don’t settle for a flat list.
- Use Microsoft Graph
/transitiveMembersfor cloud truth; reconstruct lineage as needed. - Reduce or ban nesting into admin groups; prefer JIT/JEA over permanent elevation.
Check these out
- Trimarc: Nesting is for the birds
- Windows events: Security log encyclopedia
- Microsoft Graph: /transitiveMembers
- ManageEngine perspectives on group memberships and audits (link to vendor blog as appropriate)
- Google Cloud IAM groups best practices: strict nesting policies
- O365 & Entra PowerShell audit ideas: audit membership changes, nested groups report


