# ADGroup.ps1 # Security group management. # No dependencies on other AD modules. function Ensure-ADSecurityGroup { <# .SYNOPSIS Idempotently creates a security group and syncs membership. #> param( [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Path, [string]$Description = '', [ValidateSet('Global', 'DomainLocal', 'Universal')] [string]$Scope = 'Global', [string[]]$Members = @() ) $created = $false try { $group = Get-ADGroup -Identity $Name -ErrorAction Stop Write-Host " [OK] Group exists: $Name" -ForegroundColor Green } catch { New-ADGroup -Name $Name -Path $Path -GroupScope $Scope -GroupCategory Security ` -Description $Description -ProtectedFromAccidentalDeletion $true Write-Host " [CREATED] Group: $Name" -ForegroundColor Yellow $created = $true } # Ensure protected from accidental deletion $group = Get-ADGroup -Identity $Name -Properties ProtectedFromAccidentalDeletion if (-not $group.ProtectedFromAccidentalDeletion) { Set-ADObject -Identity $group.DistinguishedName -ProtectedFromAccidentalDeletion $true Write-Host " [UPDATED] $Name ProtectedFromAccidentalDeletion=True" -ForegroundColor Yellow } # Sync membership if ($Members.Count -gt 0) { $currentMembers = @(Get-ADGroupMember -Identity $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SamAccountName) foreach ($member in $Members) { if ($member -notin $currentMembers) { Add-ADGroupMember -Identity $Name -Members $member Write-Host " [ADDED] $member -> $Name" -ForegroundColor Yellow } } # Remove members not in desired list foreach ($current in $currentMembers) { if ($current -notin $Members) { Remove-ADGroupMember -Identity $Name -Members $current -Confirm:$false Write-Host " [REMOVED] $current from $Name" -ForegroundColor Red } } } return $created } function Compare-ADSecurityGroup { <# .SYNOPSIS Compares desired group state against AD. Returns diffs. #> param( [Parameter(Mandatory)] [string]$Name, [string[]]$Members = @() ) $diffs = @() try { $group = Get-ADGroup -Identity $Name -Properties ProtectedFromAccidentalDeletion -ErrorAction Stop Write-Host " [OK] Group exists: $Name" -ForegroundColor Green # Check protection if (-not $group.ProtectedFromAccidentalDeletion) { Write-Host " [DRIFT] $Name not protected from accidental deletion" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GroupProtection'; Group = $Name; Status = 'Unprotected' } } # Check membership $currentMembers = @(Get-ADGroupMember -Identity $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SamAccountName) foreach ($member in $Members) { if ($member -notin $currentMembers) { Write-Host " [DRIFT] $member not in group $Name" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GroupMember'; Group = $Name; Member = $member; Status = 'Missing' } } } foreach ($current in $currentMembers) { if ($current -notin $Members) { Write-Host " [DRIFT] $current in group $Name but not in desired state" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GroupMember'; Group = $Name; Member = $current; Status = 'Extra' } } } } catch { Write-Host " [MISSING] Group: $Name" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'Group'; Name = $Name; Status = 'Missing' } } return $diffs }