<# .SYNOPSIS Lists all Active Directory security groups that have no members. .DESCRIPTION Uses Active Directory cmdlets to retrieve every security group and checks whether each one has any members. Groups with no members are returned as objects (GroupName, CanonicalName, DistinguishedName). By default the whole domain is searched; optionally the search can be limited to one or more Organizational Units, searched recursively. .PARAMETER SearchBase One or more OU distinguished names to search recursively for security groups. Optional - if omitted, the entire domain is searched. .EXAMPLE .\Get-EmptySecurityGroups.ps1 Displays all empty security groups in Active Directory. .EXAMPLE .\Get-EmptySecurityGroups.ps1 | Export-Csv -Path .\Export_EmptySecurityGroups.csv -NoTypeInformation -Encoding UTF8 Exports all empty security groups in Active Directory to a CSV file. .EXAMPLE .\Get-EmptySecurityGroups.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com" Searches recursively only within the Sales OU. .EXAMPLE .\Get-EmptySecurityGroups.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com","OU=IT,DC=contoso,DC=com" Searches recursively within multiple OUs. .NOTES Author: Petr Štěpán Created: 2024-10-20 Version: 1.1.0 Changelog: 1.1.0 - Added optional -SearchBase parameter for recursive search within specific OUs. 1.0.0 - Initial version #> #Requires -Modules ActiveDirectory [CmdletBinding()] param ( [Parameter(Mandatory = $false, HelpMessage = 'One or more OU distinguished names to search recursively. Defaults to the entire domain.')] [ValidateNotNullOrEmpty()] [string[]] $SearchBase ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # Read-only script (queries Active Directory only) - no ShouldProcess needed. $emptyGroups = [System.Collections.Generic.List[PSCustomObject]]::new() $counter = 0 if ($SearchBase) { $securityGroups = [System.Collections.Generic.List[object]]::new() $failedOuCount = 0 foreach ($ou in $SearchBase) { Write-Verbose "Retrieving security groups from OU '$ou' (recursive)..." try { $ouGroups = Get-ADGroup -Filter { GroupCategory -eq 'Security' } -SearchBase $ou -SearchScope Subtree -ErrorAction Stop foreach ($g in $ouGroups) { $securityGroups.Add($g) } } catch { Write-Warning "Skipping OU '$ou': failed to query security groups - $_" $failedOuCount++ } } if ($failedOuCount -eq $SearchBase.Count) { Write-Error "Failed to retrieve security groups from any of the specified OUs." exit 1 } } else { Write-Verbose "Retrieving all security groups from Active Directory..." try { $securityGroups = @(Get-ADGroup -Filter { GroupCategory -eq 'Security' } -ErrorAction Stop) } catch { Write-Error "Failed to retrieve security groups from Active Directory: $_" exit 1 } } foreach ($group in $securityGroups) { $counter++ Write-Progress -Activity "Checking group $($group.Name)" -Status "Processing $counter of $($securityGroups.Count)" -PercentComplete (($counter / $securityGroups.Count) * 100) try { $members = @(Get-ADGroupMember -Identity $group.DistinguishedName -ErrorAction Stop) } catch { Write-Warning "Skipping group '$($group.Name)': failed to read members - $_" continue } if ($members.Count -eq 0) { # Record the empty group as a structured object $emptyGroups.Add([PSCustomObject]@{ GroupName = $group.Name Description = $group.Description DistinguishedName = $group.DistinguishedName }) } } Write-Progress -Activity "Checking group" -Completed if ($emptyGroups.Count -gt 0) { $emptyGroups } else { Write-Host "No empty security groups were found." } exit 0