Public/Miscellaneous/Remove-UnknownSID.ps1

Function Remove-UnknownSID {
    <#
        .SYNOPSIS
            Removes unresolvable SIDs from Active Directory object permissions.

        .DESCRIPTION
            This function identifies and optionally removes unresolvable Security Identifiers (SIDs) from
            the Access Control Entries (ACEs) of specified Active Directory objects. An unresolvable SID
            typically indicates a deleted security principal (user, group, or computer).

            The function:
            - Identifies SIDs that cannot be resolved to friendly names
            - Excludes well-known SIDs from removal
            - Supports both audit and removal modes
            - Processes single objects or pipeline input
            - Implements proper transaction handling for ACL modifications

        .PARAMETER LDAPpath
            The Distinguished Name of the Active Directory object to check for unresolvable SIDs.
            This parameter accepts pipeline input and must be a valid DN format.

        .PARAMETER RemoveSID
            Switch parameter that determines whether to remove the unresolvable SIDs.
            If not specified, the function will only report the unresolvable SIDs.

        .EXAMPLE
            Remove-UnknownSID -LDAPpath "OU=Users,OU=Good,OU=Sites,DC=EguibarIT,DC=local"

            Lists all unresolvable SIDs found in the specified OU without removing them.

        .EXAMPLE
            Remove-UnknownSID -LDAPpath "OU=Users,OU=Good,OU=Sites,DC=EguibarIT,DC=local" -RemoveSID

            Removes all unresolvable SIDs from the specified OU's ACL.

        .EXAMPLE
            Get-ADOrganizationalUnit -Filter * | Remove-UnknownSID -RemoveSID

            Removes unresolvable SIDs from all OUs in the domain.

        .OUTPUTS
            [void]

        .NOTES
            Used Functions:
                Name ║ Module
                ═════════════════════════════════════╬══════════════════════════════
                Test-IsValidDN ║ EguibarIT.DelegationPS
                Get-AdWellKnownSID ║ EguibarIT.DelegationPS
                Convert-SidToName ║ EguibarIT.DelegationPS
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Warning ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility

        .NOTES
            Version: 1.1
            DateModified: 24/Mar/2025
            LasModifiedBy: Vicente Rodriguez Eguibar
                vicente@eguibar.com
                Eguibar IT
                http://www.eguibarit.com

        .LINK
            https://github.com/vreguibar/EguibarIT.DelegationPS/blob/main/Public/Miscellaneous/Remove-UnknownSID.ps1

        .LINK
            https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers

        .COMPONENT
            ActiveDirectory

        .ROLE
            Security Administration
    #>


    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    [OutputType([void])]

    Param (
        # PARAM1 STRING for the Object Name
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Distinguished Name of the object (or container) where the Unknown SID is located.',
            Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            { Test-IsValidDN -ObjectDN $_ },
            ErrorMessage = 'DistinguishedName provided is not valid! Please Check.'
        )]
        [Alias('DN', 'DistinguishedName')]
        [String]
        $LDAPpath,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Switch indicator to remove the unknown SID.',
            Position = 1)]
        [ValidateNotNullOrEmpty()]
        [switch]
        $RemoveSID,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            HelpMessage = 'If present, the function will not ask for confirmation when performing actions.',
            Position = 2)]
        [Switch]
        $Force
    )

    Begin {

        Set-StrictMode -Version Latest

        # Display function header if variables exist
        if ($null -ne $Variables -and $null -ne $Variables.HeaderDelegation) {
            $txt = ($Variables.HeaderDelegation -f
                (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
                (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end if

        ##############################
        # Module imports

        ##############################
        # Variables Definition
        $myObject = $null
        $AllRules = $null
        $rule = $null

    } # end Begin

    Process {

        # Get the LDAP object to get the access rules from
        $myObject = [System.DirectoryServices.DirectoryEntry]::new('LDAP://{0}' -f $PSBoundParameters['LDAPPath'])

        if (-not $myObject) {
            throw ('Failed to access object: {0}' -f $LDAPpath)
        }

        # Get the access rules
        $AllRules = $myObject.ObjectSecurity.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])

        Write-Verbose -Message ('Found {0} rules in this object. Iterating through them.' -f $AllRules.count)

        # Iterate through all Access Control rules
        foreach ($rule in $AllRules) {
            #$oar = $rule -as [System.Security.AccessControl.ActiveDirectoryAccessRule]

            # Check if WellKnownSid. Skipping WellKnownSIDs. Continue if not.
            if (-not (Get-AdWellKnownSID -Sid ($rule.IdentityReference.Value))) {


                # Translate SID. True if exists. False if it does not exist.
                if (-not (Convert-SidToName -Sid $rule.IdentityReference.ToString())) {

                    Write-Verbose -Message ('Unresolved SID found! {0}' -f $($rule.IdentityReference.ToString()))

                    if ($PSBoundParameters['RemoveSID']) {

                        Write-Verbose -Message 'Preparing to remove un-resolved SID'

                        try {
                            # Remove unknown SID from rule
                            If ($Force -or
                                $PSCmdlet.ShouldProcess($PSBoundParameters['LDAPpath'], 'Remove unknown SID?')) {

                                $myObject.ObjectSecurity.RemoveAccessRule($rule)

                            } #end If

                            Write-Verbose -Message ('Successfully removed SID: {0}' -f $rule.IdentityReference)

                        } catch {

                            Write-Error -Message ('Failed to remove SID {0}: {1}' -f $rule.IdentityReference, $_.Exception.Message)
                            continue

                        } #end Try-Catch

                    } else {

                        Write-Warning -Message ('Unresolvable SID found: {0}' -f $rule.IdentityReference.ToString())

                    } #end If-Else
                } #end If
            } #end If
        } #end ForEach

        # Commit changes if any SIDs were removed
        if ($RemoveSID -and $unknownSidsFound -gt 0) {
            try {
                # Re-apply the modified DACL to the OU
                # Now push these AccessRules to AD
                $myObject.CommitChanges()

                Write-Verbose -Message ('Successfully committed changes to {0}' -f $LDAPpath)

            } catch {
                throw [System.ApplicationException]::new("An error occurred while committing changes to the access rule: '$($_.Exception)'. Message is $($_.Exception.Message)")
            } #end Try-Catch
        } #end if

    } # end Process

    End {

        if ($null -ne $Variables -and
            $null -ne $Variables.FooterDelegation) {

            $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName,
                'detecting and removing unresolved SIDs.'
            )
            Write-Verbose -Message $txt
        } #end If

        if ($null -ne $myObject) {
            $myObject.Dispose()
        }

    } #end END
} #end function Remove-UnknownSID