Public/Remove-OrphanedSid.ps1
|
function Remove-OrphanedSid { <# .EXTERNALHELP OrphanedSid-Help.xml .SYNOPSIS Removes orphaned access control entries from Active Directory objects after backing up each ACL. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType('RemovalOperationResult')] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('DistinguishedName')] [string[]]$SearchBase, [Parameter()] [ValidateScript({ $parent = Split-Path -Parent $_ if ($parent -and -not (Test-Path -Path $parent -PathType Container)) { throw "The parent folder for -BackupPath does not exist: $parent" } $true })] [string]$BackupPath, [Parameter()] [switch]$Force, [Parameter()] [ValidateScript({ $directory = Split-Path -Parent $_ if (-not $directory) { $directory = (Get-Location).Path } if (-not (Test-Path -Path $directory -PathType Container)) { throw "The folder for -OutputPath does not exist: $directory" } $true })] [string]$OutputPath, [Parameter()] [switch]$IncludeInherited, [Parameter()] [ValidateRange(1, 10)] [int]$MaxRetries = 3, [Parameter()] [ValidateScript({ if ($_ -and -not (Test-Path -Path $_ -PathType Leaf)) { throw "Configuration file not found: $_" } $true })] [string]$ConfigPath, [Parameter()] [ValidateRange(100, 16384)] [int]$MaxMemoryUsageMB = 1024, [Parameter()] [switch]$PreserveTempFiles, [Parameter()] [string]$LogPath, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString(), [Parameter()] [ValidateSet('Critical', 'Error', 'Warning', 'Information', 'Debug', 'Verbose')] [string]$LogLevel = 'Information', [Parameter()] [switch]$SuppressConsoleOutput ) begin { # -Force skips the ConfirmImpact High prompt. -WhatIf is unaffected because it does not # go through $ConfirmPreference. if ($Force) { $ConfirmPreference = 'None' } $sessionParams = @{ CommandName = $MyInvocation.MyCommand.Name ConfigPath = $ConfigPath LogPath = $LogPath MaxMemoryUsageMB = $MaxMemoryUsageMB CorrelationId = $CorrelationId LogLevel = $LogLevel LogLevelBound = $PSBoundParameters.ContainsKey('LogLevel') SuppressConsoleOutput = $SuppressConsoleOutput RequireActiveDirectory = $true } $null = Start-OrphanedSidSession @sessionParams $searchBases = [System.Collections.Generic.List[string]]::new() } process { foreach ($dn in $SearchBase) { if (-not [string]::IsNullOrWhiteSpace($dn)) { $searchBases.Add($dn.Trim()) } } } end { try { if ($searchBases.Count -eq 0) { Write-Warning 'No search base specified. Using the current domain root.' $searchBases.Add((Get-ADDomain).DistinguishedName) } $target = $searchBases -join ', ' if (-not $PSCmdlet.ShouldProcess($target, 'Remove orphaned SIDs from ACLs (a backup of each ACL is written first)')) { # Under -WhatIf the discovery still runs so the preview lists what would be removed; # Invoke-MainProcessingLogic honours $WhatIfPreference for the removal itself. if (-not $WhatIfPreference) { Write-StructuredLog "Removal declined at the confirmation prompt for: $target" -Level Warning -Component 'Main' -CorrelationId $CorrelationId return } } $effectiveBackupPath = $BackupPath if (-not $effectiveBackupPath) { $effectiveBackupPath = Join-Path -Path (Get-Location).Path -ChildPath 'Backup' } $effectiveBackupPath = Join-Path -Path $effectiveBackupPath -ChildPath (Get-Date -Format 'yyyyMMdd_HHmmss') if (-not $WhatIfPreference -and -not (Test-Path -Path $effectiveBackupPath)) { New-Item -Path $effectiveBackupPath -ItemType Directory -Force | Out-Null Write-StructuredLog "Created backup folder: $effectiveBackupPath" -Level Information -Component 'Main' -CorrelationId $CorrelationId } Write-StructuredLog "Starting removal for: $target" -Level Information -Component 'Main' -CorrelationId $CorrelationId $processingParams = @{ SearchBase = $searchBases.ToArray() Remove = $true IncludeInherited = $IncludeInherited BackupPath = $effectiveBackupPath MaxRetries = $MaxRetries CorrelationId = $CorrelationId } $processingResults = Invoke-MainProcessingLogic @processingParams foreach ($removal in @($processingResults.RemovalResults)) { Write-Output $removal } Write-ProcessingSummary -ProcessingResults $processingResults -OutputPath $OutputPath -CorrelationId $CorrelationId Write-StructuredLog "Removal completed (CorrelationId: $CorrelationId)" -Level Information -Component 'Main' -CorrelationId $CorrelationId } catch { if ($script:Statistics) { $script:Statistics.CriticalErrors++ } Write-StructuredLog "Removal failed: $($_.Exception.Message)" -Level Error -Component 'Main' -CorrelationId $CorrelationId Write-Error -ErrorRecord $_ -ErrorAction Stop } finally { Stop-OrphanedSidSession -PreserveTempFiles:$PreserveTempFiles -CorrelationId $CorrelationId } } } |