Shore Up
A row of filing cabinet folders marked with a red "closed" stamp, but each still tied by a string to a set of keys on a ring, and a hand about to snip the strings.
WindowsSecurity

Find Disabled AD Users That Still Hold Group Membership

Ketan Aagja8 min read
No ratings yet

A disabled account is not a harmless account. If it still holds membership in security groups, it still carries the access rights those groups grant — file shares, application roles, delegated permissions. If that account is ever re-enabled (by mistake, or by an attacker who compromised a helpdesk process), it lights up with all its old access instantly. Periodically finding disabled users that still sit in security groups is a basic hygiene task, and it is easy to script.

This guide does the finding first as a read-only report, then shows an optional, gated cleanup step.

Before you run this

The detection script only reads from Active Directory. It enumerates disabled user accounts, resolves each group they belong to, and writes a CSV of the ones that are security groups. It changes nothing. Any account that can read the directory (essentially any authenticated domain user) can run the report part.

The optional remediation script at the end removes those accounts from those groups. That is a change to your directory. Removing membership is reversible in the sense that you can add the user back with Add-ADGroupMember, but you lose the "when added" history and any downstream automation that keyed off that membership will have already reacted. Treat it as a real change. It needs an account with rights to modify the affected groups — a delegated group manager for those specific groups, or a Domain Admin. Run it from an elevated PowerShell session.

Both scripts require the ActiveDirectory module (part of RSAT). On a domain-joined admin workstation running Windows 10/11, install RSAT's "Active Directory Domain Services and Lightweight Directory Services Tools"; on a member/domain-controller server it comes with the AD DS role or RSAT-AD-PowerShell feature.

Before you touch the remediation step: run the report first, read the CSV, and test the removal against a single throwaway test user in a single test group before you let it loose on the full list. Never pipe the whole CSV into Remove-ADGroupMember blind on a Friday afternoon.

Assumptions for this guide: Windows Server 2019/2022 or a Windows 10/11 admin workstation, Windows PowerShell 5.1, the ActiveDirectory module present, and a single-domain forest. The cmdlets are the same on PowerShell 7 with the module imported via the Windows compatibility session; the syntax below does not change.

The detection script

Save this as something like Find-DisabledUsersInSecurityGroups.ps1. Replace the CSV path with a real path you can write to.

Import-Module ActiveDirectory

# Pull every disabled user and its group memberships in one query.
# MemberOf deliberately does NOT include the primary group (usually
# Domain Users), so we won't flag every account for that.
$disabled = Get-ADUser -Filter 'Enabled -eq $false' -Properties MemberOf

# Cache group lookups so we resolve each group only once, not once per user.
$groupCache = @{}

$report = foreach ($user in $disabled) {
    foreach ($groupDN in $user.MemberOf) {

        if (-not $groupCache.ContainsKey($groupDN)) {
            $groupCache[$groupDN] =
                Get-ADGroup -Identity $groupDN -Properties GroupCategory
        }
        $group = $groupCache[$groupDN]

        # We only care about security groups, not distribution groups.
        if ($group.GroupCategory -eq 'Security') {
            [pscustomobject]@{
                UserName  = $user.SamAccountName
                UserDN    = $user.DistinguishedName
                GroupName = $group.Name
                GroupDN   = $group.DistinguishedName
            }
        }
    }
}

# Write the report and show it on screen.
$report | Sort-Object UserName, GroupName |
    Export-Csv -Path 'C:\path\to\disabled-users-security-groups.csv' -NoTypeInformation

$report | Sort-Object UserName, GroupName | Format-Table -AutoSize

A few things worth knowing about what this does and does not catch:

  • The primary group is excluded on purpose. MemberOf never lists an account's primary group (Domain Users for a normal user). That is exactly what you want — you are not trying to flag every disabled user for belonging to Domain Users, you are looking for the extra group memberships that grant real access.
  • Distribution groups are filtered out by the GroupCategory -eq 'Security' check. If you want to see distribution-group membership too, drop that if.
  • If you only want to audit one OU, add -SearchBase 'OU=Staff,DC=example,DC=com' to the Get-ADUser call. Replace that distinguished name with yours.

In a very large directory the Get-ADUser query returns everything at once; if that is a concern, add -ResultPageSize or scope with -SearchBase. For most environments the query as written is fine.

Reading the report

Open the CSV. Each row is one disabled user in one security group. Look especially for:

  • Membership in privileged groups (Domain Admins, Enterprise Admins, Server Operators, Account Operators, any custom admin role).
  • Accounts disabled long ago that still sit in application or share groups.

Decide group by group whether the membership should go. Some organisations deliberately keep disabled accounts in groups for a grace period, or for licence/attribute reasons. This is a judgement call, which is why the cleanup is a separate, opt-in step rather than baked into the detection.

The optional cleanup — test with -WhatIf first

Do not modify the CSV between running the report and running this — it uses the exact DNs the report captured. Run it first with -WhatIf so you see every change it would make without making any:

Import-Module ActiveDirectory

Import-Csv -Path 'C:\path\to\disabled-users-security-groups.csv' | ForEach-Object {
    # -WhatIf: shows the action, changes nothing. Remove it to act for real.
    Remove-ADGroupMember -Identity $_.GroupDN -Members $_.UserDN -WhatIf
}

Read that output carefully. When you are satisfied, remove -WhatIf. Remove-ADGroupMember prompts for confirmation by default on each removal, which is a useful safety net; if you have reviewed the CSV and want it to run unattended, add -Confirm:$false — but only once you trust the list.

Note: if any row somehow references a user's primary group, Remove-ADGroupMember will refuse it with an error. That is correct behaviour; you change a primary group by reassigning it, not by removing membership, and it is outside the scope of this cleanup.

For the exact parameters and behaviour of these cmdlets, see Microsoft Learn for Remove-ADGroupMember, Get-ADUser, and Get-ADGroup.

Verify it worked

Re-run the detection script. The users you cleaned up should no longer appear in the CSV (or appear only for the groups you chose to leave them in).

To spot-check a single account directly, list its remaining group memberships:

# Replace with a SamAccountName you just cleaned up.
Get-ADPrincipalGroupMembership -Identity 'jdoe' |
    Select-Object Name, GroupCategory

After cleanup, that account should show only its primary group (Domain Users) and any security groups you deliberately kept.

Undo

If you removed a membership you should not have, add it back:

# Replace with the real group and user distinguished names.
Add-ADGroupMember -Identity 'CN=App-Readers,OU=Groups,DC=example,DC=com' `
    -Members 'CN=John Doe,OU=Staff,DC=example,DC=com'

This is why the report CSV matters: keep it. It is your record of exactly what each account belonged to before you changed anything, so you can restore precisely. Save a dated copy of the CSV alongside your change record before you run the cleanup, and you have a clean, honest audit trail of what you removed and from where.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Audit Local Administrators Across Many Windows Machines

This is a read-only audit. It pulls a list of computers from Active Directory, connects to each one over PowerShell Remoting (WinRM), reads the membership of the local Administrators group, and writes everything to a single CSV you can open in Excel. It creates nothing and changes nothing on the target machines, so there is no destructive step and nothing to roll back — the only thing produced is the report file on your own workstation.

9 min read

Clean Up Stale Computer Accounts in Active Directory with PowerShell

This guide finds Active Directory computer accounts that haven't logged in for a long time and retires them in three deliberate stages: report , disable and move to a holding OU , then delete . The purpose is to keep AD tidy and reduce the attack surface of forgotten machine accounts without accidentally killing a computer that's simply been powered off for a while.

8 min read

Find and Report AD Accounts That Have Never Logged In

This guide gives you a PowerShell script that reads Active Directory and produces a report (on screen and as a CSV) of user accounts that have never authenticated against any domain controller. It changes nothing — it does not disable, delete, or edit a single account. Its purpose is to hand you a clean list of candidates for review before you decide what to do with them.

9 min read