From 7aeb3b5553590a121b328e16632169f7eab070c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0t=C4=9Bp=C3=A1n?= Date: Tue, 21 Jul 2026 14:34:37 +0200 Subject: [PATCH] Add Get-EmptySecurityGroups script to list empty Active Directory security groups --- Get-EmptySecurityGroups.ps1 | 118 ++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 Get-EmptySecurityGroups.ps1 diff --git a/Get-EmptySecurityGroups.ps1 b/Get-EmptySecurityGroups.ps1 new file mode 100644 index 0000000..a678872 --- /dev/null +++ b/Get-EmptySecurityGroups.ps1 @@ -0,0 +1,118 @@ +<# +.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