Last week we had a client who wanted to delete ALL disabled user accounts because they had been hacked and wanted to reduce their attack surface. The two problems we had with this is that there are accounts that are disabled but may be needed in the future, and there were thousands of disabled accounts. To make this slightly more complex the client wanted to exclude (i.e. keep) 3 user accounts that were disabled. Doing this manually is a non-starter; a script is required.

After a few hours of testing this is what we came up with:

Script To List All Disabled Users

This is a great place to start. You can then look at the list, check a few of them in AD to make sure these users are disabled and perhaps consider a few that might be needed in the future

# Define the users to exclude
$excludedUsers = @("Cudy Rang", "Rob Chura", "Lori Post")

# Get disabled user accounts
$disabledUsers = Get-ADUser -Filter {Enabled -eq $false} -Properties DisplayName, GivenName, Surname, UserPrincipalName, WhenCreated, Location, Description, CanonicalName | Where-Object {
    $excludedUsers -notcontains "$($_.GivenName) $($_.Surname)"
}

# Create a custom object for output
$results = $disabledUsers | Select-Object @{Name='LastName';Expression={$_.Surname}},
                                        @{Name='FirstName';Expression={$_.GivenName}},
                                        @{Name='DisplayName';Expression={$_.DisplayName}},
                                        @{Name='UPN';Expression={$_.UserPrincipalName}},
                                        @{Name='Created';Expression={$_.WhenCreated}},
                                        @{Name='Location';Expression={$_.Location}},
                                        @{Name='Description';Expression={$_.Description}},
                                        @{Name='CanonicalName';Expression={$_.CanonicalName}},
                                        @{Name='Status';Expression={'Disabled'}}

# Sort the results by last name
$sortedResults = $results | Sort-Object LastName

# Output to the screen and a CSV file
$sortedResults | Export-Csv -Path "C:\temp\disabled.csv" -NoTypeInformation
$sortedResults | Format-Table -AutoSize

Script to Delete All Disabled Users with a First & Last Name

We wanted to restrict the deletion to accounts that were likely created by humans, and one way to do that is to delete only disabled accounts with a first and last name:

# Define the users to exclude
$excludedUsers = @("Cudy Rang", "Rob Chura", "Lori Post")

# Get disabled user accounts
$disabledUsers = Get-ADUser -Filter {Enabled -eq $false} -Properties DisplayName, GivenName, Surname | Where-Object {
    $_.GivenName -and $_.Surname -and ($excludedUsers -notcontains "$($_.GivenName) $($_.Surname)")
}

# Create a custom object for output
$results = $disabledUsers | Select-Object @{Name='LastName';Expression={$_.Surname}},
                                        @{Name='FirstName';Expression={$_.GivenName}},
                                        @{Name='DisplayName';Expression={$_.DisplayName}},
                                        @{Name='Status';Expression={'Disabled'}}

# Sort the results by last name
$sortedResults = $results | Sort-Object LastName

# Output the results to a CSV file
$sortedResults | Export-Csv -Path "C:\temp\disabled.csv" -NoTypeInformation

# Output the results
$sortedResults | Format-Table -AutoSize

This resulted in a few thousand accounts being deleted, but dozens of errors like:

Remove-ADUser : The directory service can perform the requested operation only on a leaf object


Remove-ADUser The directory service can perform the requested operation only on a leaf object

So, that gets us to what is a “leaf object”. Basically, it is and object that has other objects under it. For example, if you were trying to delete a computer from Active Directory you might receive a similar message if that computer has a printer that is shared out. In this case it was because these disabled users still had devices tied to Exchange email accounts:


what objects are contained under a ad user account

Here is the script we used:

# Define the user account
$user = Get-ADUser -Identity "YourUser" -Properties *

# Get child objects
$childObjects = Get-ADObject -SearchBase $user.DistinguishedName -Filter *

# Display child objects
$childObjects | Format-Table Name, ObjectClass

After we were satisfied that all of those accounts were just user accounts that had not been properly decommissioned, we added changed the last segment of the delete script to:

# Delete the filtered user accounts
$disabledUsers | ForEach-Object {
    Remove-ADObject -Identity $_ -Recursive -Confirm:$false
}

Script To Delete All Disabled Users Except a Few Specified & Those Without A First or Last Name

# Define the users to exclude
$excludedUsers = @("Cudy Rang", "Rob Chura", "Lori Post")

# Get disabled user accounts
$disabledUsers = Get-ADUser -Filter {Enabled -eq $false} -Properties DisplayName, GivenName, Surname | Where-Object {
    $_.GivenName -and $_.Surname -and ($excludedUsers -notcontains "$($_.GivenName) $($_.Surname)")
}

# Create a custom object for output
$results = $disabledUsers | Select-Object @{Name='LastName';Expression={$_.Surname}},
                                        @{Name='FirstName';Expression={$_.GivenName}},
                                        @{Name='DisplayName';Expression={$_.DisplayName}},
                                        @{Name='Status';Expression={'Disabled'}}

# Sort the results by last name
$sortedResults = $results | Sort-Object LastName

# Output the results to the screen and CSV file
$sortedResults | Format-Table -AutoSize
$sortedResults | Export-Csv -Path "C:\temp\deleted.csv" -NoTypeInformation

# Delete the filtered user accounts
$disabledUsers | ForEach-Object {
    Remove-ADObject -Identity $_ -Recursive -Confirm:$false
}

After that, we ran the first script again to list all of the user accounts that were disabled but not yet deleted, so the client could go through them one by one and decide if the accounts really should be deleted.



0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *