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.

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.

Check these out

Exit mobile version