- Published on
How to Report on Domain Admins and Other Privileged Groups in Active Directory (PowerShell Script)
"Who is in Domain Admins" sounds like a simple question until someone is added through a nested group three layers deep and Get-ADGroupMember without -Recursive misses them entirely. Auditors ask for this list constantly, and it's exactly the kind of report that's wrong by omission if you don't handle nesting correctly.
The quick answer
Get-ADGroupMember -Identity "Domain Admins" -Recursive
-Recursive is the part people forget — without it, a user who's only in Domain Admins via a nested group won't show up at all.
A fuller report across all three built-in privileged groups
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Reports members of Active Directory's privileged admin groups.
.DESCRIPTION
Recursively lists the members of Domain Admins, Enterprise Admins, and
Schema Admins (the built-in AD groups with domain- or forest-wide
control), flattening nested group membership so a user buried in a
nested group still shows up. Flags stale privileged accounts — enabled
members that haven't logged on within the given threshold — since
privileged accounts are exactly the ones you don't want sitting around
unused. Uses the current logged-on user's credentials — run it from a
domain-joined machine with RSAT installed.
.PARAMETER StaleDays
Flag privileged accounts that haven't logged on in at least this many
days. Defaults to 90.
.PARAMETER OutputCsv
Path to a CSV file to export the results to. If omitted, results are only
written to the console.
.EXAMPLE
.\Get-ADDomainAdminsReport.ps1
Lists all members of Domain Admins, Enterprise Admins, and Schema Admins,
flagging any that haven't logged on in 90+ days.
.EXAMPLE
.\Get-ADDomainAdminsReport.ps1 -StaleDays 30 -OutputCsv .\privileged-accounts.csv
Same report with a 30-day staleness threshold, exported to CSV.
#>
[CmdletBinding()]
param(
[int]$StaleDays = 90,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$privilegedGroups = 'Domain Admins', 'Enterprise Admins', 'Schema Admins'
$cutoff = (Get-Date).AddDays(-$StaleDays)
$results = New-Object System.Collections.Generic.List[object]
foreach ($groupName in $privilegedGroups) {
try {
$members = Get-ADGroupMember -Identity $groupName -Recursive -ErrorAction Stop
} catch {
Write-Warning "Could not read group '$groupName': $($_.Exception.Message)"
continue
}
foreach ($member in $members) {
if ($member.objectClass -ne 'user') {
continue
}
$user = Get-ADUser -Identity $member.SID -Properties Enabled, LastLogonDate
$results.Add([PSCustomObject]@{
Group = $groupName
Name = $user.Name
SamAccountName = $user.SamAccountName
Enabled = $user.Enabled
LastLogonDate = $user.LastLogonDate
Stale = ($user.Enabled -and (-not $user.LastLogonDate -or $user.LastLogonDate -lt $cutoff))
})
}
}
if ($results.Count -eq 0) {
Write-Host "No members found in the privileged groups checked." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object Group, Name -Unique
Write-Host "Found $(@($sorted).Count) privileged account membership(s)." -ForegroundColor Cyan
$sorted | Format-Table Group, Name, SamAccountName, Enabled, LastLogonDate, Stale -AutoSize
$staleCount = @($sorted | Where-Object Stale).Count
if ($staleCount -gt 0) {
Write-Host "$staleCount privileged account(s) haven't logged on in $StaleDays+ days — review whether they still need this access." -ForegroundColor Red
}
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
Why the "stale" flag matters here specifically
A dormant account in a regular OU is a moderate finding. A dormant account still sitting in Domain Admins is a much bigger one — it's standing forest-wide access that nobody is actively using or watching, which is exactly the profile attackers look for. If this script flags one, the fix isn't just "disable it" — it's figuring out why it was never removed from the group when it stopped being needed.
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.
- Stale and inactive account detection — Enabled accounts that nobody has logged into for 30, 60, or 90+ days — the ones that are still a live attack surface.
- Privileged group auditing — Who is in Domain Admins, Enterprise Admins, and Schema Admins — including nested membership and accounts that should not be there.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
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-ADDomainAdminsReport.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.