119 lines
3.9 KiB
PowerShell
119 lines
3.9 KiB
PowerShell
<#
|
|
.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
|