- Published on
How to Find a User's Full Group Membership (Including Nested Groups) in Active Directory (PowerShell Script)
"Why does this user have access to X" is one of the most common troubleshooting questions in AD, and the obvious first move — checking the user's MemberOf property — only gives you half the answer. MemberOf lists direct memberships only. If the user is in Group A, and Group A is a member of Group B, the user effectively has Group B's access, but MemberOf won't mention Group B at all.
The quick answer
For direct memberships only:
(Get-ADUser -Identity jdoe -Properties MemberOf).MemberOf
That's a starting point, not the full picture — it stops at one level of nesting.
A script that walks the full nesting chain
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Shows every Active Directory group a user belongs to, including nested membership.
.DESCRIPTION
A user's MemberOf property only lists groups they're a direct member of
— it won't show a group they belong to because they're a member of
another group that's a member of it. This walks the membership chain
recursively and reports every group in the resulting tree along with how
many levels of nesting it took to get there, which is what you actually
need when troubleshooting "why does this user have access to X". Uses
the current logged-on user's credentials — run it from a domain-joined
machine with RSAT installed.
.PARAMETER SamAccountName
The user's SAM account name (e.g. jdoe). Required.
.PARAMETER OutputCsv
Path to a CSV file to export the results to. If omitted, results are only
written to the console.
.EXAMPLE
.\Get-ADNestedGroupMembership.ps1 -SamAccountName jdoe
Shows every group jdoe belongs to, directly or through nesting.
.EXAMPLE
.\Get-ADNestedGroupMembership.ps1 -SamAccountName jdoe -OutputCsv .\jdoe-groups.csv
Same, exported to CSV.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SamAccountName,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop
$visited = New-Object System.Collections.Generic.HashSet[string]
$results = New-Object System.Collections.Generic.List[object]
function Resolve-GroupChain {
param(
[string[]]$GroupDNs,
[int]$Level
)
foreach ($dn in $GroupDNs) {
if (-not $visited.Add($dn)) {
continue
}
$group = Get-ADGroup -Identity $dn -Properties MemberOf, GroupScope, GroupCategory
$results.Add([PSCustomObject]@{
GroupName = $group.Name
NestingLevel = $Level
GroupScope = $group.GroupScope
GroupCategory = $group.GroupCategory
DistinguishedName = $group.DistinguishedName
})
if ($group.MemberOf) {
Resolve-GroupChain -GroupDNs $group.MemberOf -Level ($Level + 1)
}
}
}
if ($user.MemberOf) {
Resolve-GroupChain -GroupDNs $user.MemberOf -Level 1
}
if ($results.Count -eq 0) {
Write-Host "$SamAccountName isn't a member of any groups." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object NestingLevel, GroupName
Write-Host "$SamAccountName belongs to $(@($sorted).Count) group(s), including nested membership." -ForegroundColor Cyan
$sorted | Format-Table GroupName, NestingLevel, GroupScope, GroupCategory -AutoSize
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
What "nesting level" tells you
The NestingLevel column isn't just trivia — a group reached through three or four levels of nesting is much easier for an access review to miss than a direct membership. If this script surfaces a user with forest-wide access five groups deep in a chain nobody remembers building, that chain itself is worth cleaning up, not just the one user's access.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Privileged group auditing — Who is in Domain Admins, Enterprise Admins, and Schema Admins — including nested membership and accounts that should not be there.
- Multi-domain and forest coverage — Enumerate every domain in the forest in one pass and get one consolidated report instead of one per domain.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Get-ADNestedGroupMembership.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.