Site icon Windows Active Directory

Auditing Nested Group Memberships: An Expert Guide

Nested group
Auditing nested group memberships for security risks: the expert’s comparison guide

Reading time: ~14–18 minLast 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.

In this guide

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.

Community advisories like Trimarc’s “nesting is for the birds” highlight how hidden admin paths sneak in via intermediate role groups. Read Trimarc’s take.

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)

Goal: detect changes fast, export path-aware nested membership for privileged groups, and explain “how” each user got access.

1) Turn on authoritative change events (AD)

  1. 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).
  2. Monitor event IDs: 4728/4729 (global), 4732/4733 (domain local), 4756/4757 (universal). Treat 4764 (group type/scope changes) as high-signal configuration drift.
  3. 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 track visited.

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 /transitiveMembers for cloud truth; reconstruct lineage as needed.
  • Reduce or ban nesting into admin groups; prefer JIT/JEA over permanent elevation.

Sources and further reading

Exit mobile version