Public/Miscellaneous/Get-AclAccessRule.ps1

Function Get-AclAccessRule {
    <#
        .SYNOPSIS
            Retrieves and displays Access Control Entries (ACEs) of an Active Directory object.

        .DESCRIPTION
            This function retrieves and displays the Access Control Entries (ACEs) of a specified Active Directory object.
            It can filter the results by identity reference. The function supports both pipeline input and batch processing
            for efficient handling of multiple objects. Use this function to analyze and audit permissions on AD objects.

            The Tool will return an arrayList with the following information:

            ACENumber : 1
            Id : Everyone
            AdRight : DeleteTree, Delete
            AccessControlType : Deny
            ObjectType : All [GuidNULL]
            AdSecurityInheritance : None
            InheritedObjectType : All [GuidNULL]
            IsInherited : False

            Explanation:
            * ACENumber: Sequential number of the ACE
            * Id: Identity Reference (trustee / SamAccountName)
            * LDAPpath: Distinguished Name of the object
            * AdRight: Active Directory rights granted
                ('AccessSystemSecurity', 'CreateChild', 'DeleteChild', 'Delete', 'DeleteTree', 'ExtendedRight',
                'GenericAll', 'GenericExecute', 'GenericRead', 'GenericWrite', 'ListChildren', 'ListObject',
                'ReadControl', 'ReadProperty', 'Self', 'Synchronize', 'WriteDacl', 'WriteOwner' or 'WriteProperty')
            * AccessControlType: Allow or Deny
            * ObjectType: GUID of the object type (translated to readable name)
            * AdSecurityInheritance: Inheritance type
                ('None', 'All', 'Descendents', 'SelfAndChildren', 'Children')
            * InheritedObjectType: GUID of the inherited object type (translated to readable name)
            * IsInherited: Whether the ACE is inherited (True or False)

        .FUNCTIONALITY
            Output from this function can be used by the Set-AclConstructor* functions to create new ACEs
             and apply them to objects. As a guidance, use:

                * Set-AclConstructor4 when we have available ID, AdRight, AccessControlType and ObjectType
                    Get-AclAccessRule4 output
                        ACENumber : 1
                        DistinguishedName : CN=Schema,CN=Configuration,DC=EguibarIT,DC=local
                        Id : EguibarIT\XXXX
                        ActiveDirectoryRights : ExtendedRight
                        AccessControlType : Allow
                        ObjectType : Change Schema Master [Extended Rights]
                        X InheritanceType : None
                        X InheritedObjectType : GuidNULL
                        X IsInherited : False

                        $Splat = @{
                            LDAPPath = 'CN=Schema,CN=Configuration,DC=EguibarIT,DC=local'
                            Id = 'EguibarIT\XXXX'
                            AdRight = 'ExtendedRight'
                            AccessControlType = 'Allow'
                            ObjectType = 'Change Schema Master [Extended Rights]'
                        }
                        Set-AclConstructor4 @Splat

                * Set-AclConstructor5 when we have same as above, plus AdSecurityInheritance
                    Get-AclAccessRule5 output
                        ACENumber : 1
                        DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
                        Id : EguibarIT\XXXX
                        AdRight : ReadProperty, WriteProperty
                        AccessControlType : Allow
                        ObjectType : siteLink [classSchema]
                        AdSecurityInheritance : All
                        X InheritedObjectType : All [GuidNULL]
                        X IsInherited : False

                        $Splat = @{
                            LDAPPath = 'CN=Sites,CN=Configuration,DC=EguibarIT,DC=local'
                            Id = 'EguibarIT\XXXX'
                            AdRight = ReadProperty, WriteProperty
                            AccessControlType = 'Allow'
                            ObjectType = 'siteLink [classSchema]'
                            AdSecurityInheritance = All
                        }
                        Set-AclConstructor5 @Splat

                * Set-AclConstructor6 when we have same as above, plus InheritedObjectType
                Get-AclAccessRule6 output
                    ACE number : 1
                    DistinguishedName : CN=Sites,CN=Configuration,DC=EguibarIT,DC=local
                    Id : EguibarIT\XXXX
                    AdRight : CreateChild, DeleteChild
                    AccessControlType : Allow
                    ObjectType : GuidNULL
                    InheritanceType : Descendents
                    InheritedObjectType : site [ClassSchema]
                    X IsInherited : False

                        $Splat = @{
                            LDAPPath = 'CN=Sites,CN=Configuration,DC=EguibarIT,DC=local'
                            Id = 'EguibarIT\XXXX'
                            AdRight = CreateChild, DeleteChild
                            AccessControlType = 'Allow'
                            ObjectType = GuidNULL
                            AdSecurityInheritance = All
                            InheritedObjectType = site [ClassSchema]
                        }
                        Set-AclConstructor6 @Splat


        .PARAMETER LDAPPath
            Distinguished Name of the Active Directory object to retrieve ACEs from.
            Multiple objects can be passed via pipeline or as an array.

        .PARAMETER SearchBy
            Optional parameter to filter ACEs by Identity Reference (Trustee).
            If provided, only ACEs matching this identity will be returned.

        .EXAMPLE
            Get-AclAccessRule -LDAPPath "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local"

            Retrieves all ACEs for the specified OU.

        .EXAMPLE
            Get-AclAccessRule "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local" "Pre-Windows 2000 Compatible Access"

            Retrieves ACEs for the specified OU, filtering only those assigned to "Pre-Windows 2000 Compatible Access".

        .EXAMPLE
            $Splat = @{
                LDAPPath = "OU=Users,OU=XXXX,OU=Sites,DC=EguibarIT,DC=local"
                SearchBy = "Pre-Windows 2000 Compatible Access"
            }
            Get-AclAccessRule @Splat

            Retrieves filtered ACEs from the specified OU

        .EXAMPLE
            Get-ADOrganizationalUnit -Filter "Name -like 'IT*'" | Get-AclAccessRule

            Retrieves ACEs for all OUs with names starting with "IT", demonstrating pipeline integration with AD cmdlets.

        .OUTPUTS
            [System.Collections.ArrayList] containing PSCustomObjects with ACE properties
            Each object contains:
            - ACENumber: Sequential number of the ACE
            - Id: Identity Reference (trustee)
            - LDAPpath: Distinguished Name of the object
            - AdRight: Active Directory rights granted
            - AccessControlType: Allow or Deny
            - ObjectType: GUID of the object type (translated to readable name)
            - AdSecurityInheritance: Inheritance type
            - InheritedObjectType: GUID of the inherited object type (translated to readable name)
            - IsInherited: Whether the ACE is inherited

        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ═══════════════════════════════════════╬══════════════════════════════
                Get-Acl ║ Microsoft.PowerShell.Security
                Set-Location ║ Microsoft.PowerShell.Management
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Progress ║ Microsoft.PowerShell.Utility
                Write-Warning ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility
                Test-IsValidDN ║ EguibarIT.DelegationPS
                Convert-GUIDToName ║ EguibarIT.DelegationPS
                Get-FunctionDisplay ║ EguibarIT.DelegationPS
                Import-MyModule ║ EguibarIT.DelegationPS

        .NOTES
            Version: 2.0
            DateModified: 21/Mar/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                            vicente@eguibar.com
                            Eguibar IT
                            http://www.eguibarit.com

        .LINK
            https://devblogs.microsoft.com/powershell-community/understanding-get-acl-and-ad-drive-output/
            https://github.com/PowerShell/Community-Blog/issues/70

        .LINK
            https://github.com/vreguibar/EguibarIT.DelegationPS/blob/main/Public/Miscellaneous/Get-AclAccessRule.ps1
    #>


    [CmdletBinding(
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]
    [OutputType([System.Collections.ArrayList])]

    param
    (
        # PARAM1 LDAP path to the object to get the ACL
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Distinguished Name of the object',
            Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            { Test-IsValidDN -ObjectDN $_ },
            ErrorMessage = 'DistinguishedName provided is not valid! Please Check.'
        )]
        [Alias('DN', 'DistinguishedName')]
        [String]
        $LDAPpath,

        # PARAM1 Search by Identity Reference
        [Parameter(Mandatory = $False,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'The identity to filter ACE',
            Position = 1)]
        [Alias('IdentityReference', 'Identity', 'Trustee', 'GroupID')]
        [String]
        $SearchBy
    )

    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
        Import-MyModule -Name 'ActiveDirectory' -Verbose:$false

        ##############################
        # Variables Definition

        [Hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase)
        [System.Collections.ArrayList]$result = [System.Collections.ArrayList]::New()
        [System.Text.StringBuilder]$sb = [System.Text.StringBuilder]::new()

        # Define ANSI escape codes for colors
        $reset = "`e[0m"    # Reset color
        $red = "`e[31m"   # Red
        $green = "`e[32m"   # Green
        $yellow = "`e[33m"   # Yellow
        $brown = "`e[33;2m" # Brown (Dark Yellow)
        $blue = "`e[34m"   # Blue
        $purple = "`e[35m"   # Purple
        $gray = "`e[90m"   # Gray
        $orange = "`e[91m"   # Orange (Bright Red)
        $green = "`e[92m"   # Bright Green
        $cyan = "`e[96m"   # Light Blue (Cyan)
        $white = "`e[97m"   # White

        # Set location to AD: drive
        try {

            Set-Location -Path 'AD:\' -ErrorAction Stop

        } catch {

            Write-Error -Message ('Failed to set location to AD:\ drive: {0}' -f $_.Exception.Message)
            return

        } #end try-catch

    } #end Begin

    Process {

        # Clear StringBuilder for new processing
        [void]$sb.Clear()
        [void]$sb.AppendLine()
        [void]$sb.AppendLine(' ACE (Access Control Entry)')

        Write-Verbose -Message ('Processing LDAP path: {0}' -f $LDAPPath)


        Try {
            # Get the ACL for the current path
            $Acl = Get-Acl -Path $PSBoundParameters['LDAPpath'] -ErrorAction Stop

            # Check if ACL was retrieved successfully
            if ($null -eq $Acl) {

                Write-Error -Message ('Failed to retrieve ACL for {0}' -f $PSBoundParameters['LDAPpath'])
                return

            } #end If

            If ($PSBoundParameters['searchBy']) {

                $AclAccess = @($Acl |
                        Select-Object -ExpandProperty Access |
                            Where-Object -FilterScript {
                                $_.IdentityReference -match $PSBoundParameters['searchBy']
                            }
                )

                [void]$sb.AppendLine(' Filtered By: {0}' -f $PSBoundParameters['SearchBy'])

            } else {

                $AclAccess = @($Acl | Select-Object -ExpandProperty Access)

            } #end If-Else

            # Check if any ACEs were found
            if ($null -eq $AclAccess -or $AclAccess.Count -eq 0) {

                Write-Warning -Message "No matching ACEs found for $($PSBoundParameters['LDAPpath'])"
                return

            } #end If


            [void]$sb.AppendLine(' LDAPpath : {0}' -f $LDAPpath)
            [void]$sb.AppendLine(' Total ACE found : {0}' -f $AclAccess.count)
            [void]$sb.AppendLine('├────────────────────────────────────────────────────────────┤')

            # Process each ACE
            $AceCount = $AclAccess.Count
            for ($i = 0; $i -lt $AceCount; $i++) {

                $Splat = @{
                    Activity        = 'Processing Access Control Entries'
                    Status          = ('Processing entry {0} of {1}' -f ($i + 1), $AceCount)
                    PercentComplete = (($i + 1) / $AceCount * 100)
                }
                # Update progress bar
                Write-Progress @Splat

                # Get the current ACE
                $entry = $AclAccess[$i]

                $ACLResult = [PSCustomObject]@{
                    ACENumber             = $i + 1
                    Id                    = $entry.IdentityReference.Value
                    LDAPpath              = $LDAPpath
                    AdRight               = $entry.ActiveDirectoryRights
                    AccessControlType     = $entry.AccessControlType
                    ObjectType            = (Convert-GUIDToName -guid $entry.ObjectType -Verbose:$false)
                    AdSecurityInheritance = $entry.InheritanceType
                    InheritedObjectType   = (Convert-GUIDToName -guid $entry.InheritedObjectType -Verbose:$false)
                    IsInherited           = $entry.IsInherited
                }
                [void]$result.Add($ACLResult)

            } #end Foreach

            # Complete the progress bar
            Write-Progress -Activity 'Processing Access Control Entries' -Completed

        } catch [System.Security.Principal.IdentityNotMappedException] {

            Write-Warning -Message ('
                Identity mapping error for {0}: {1}'
 -f $PSBoundParameters['LDAPpath'], $_.Exception.Message
            )

        } catch [System.DirectoryServices.DirectoryServicesCOMException] {

            Write-Warning -Message ('
                Directory Services error for {0}: {1}'
 -f $PSBoundParameters['LDAPpath'], $_.Exception.Message
            )

        } catch {

            Write-Error -Message ('
                Error retrieving ACL for {0}: {1}'
 -f $PSBoundParameters['LDAPpath'], $_.Exception.Message
            )

        } #end try-catch
    } #end Process

    End {
        # Display footer if variables exist
        if ($null -ne $Variables -and
            $null -ne $Variables.FooterDelegation) {

            $txt = ($Variables.FooterDelegation -f $MyInvocation.InvocationName,
                'getting ACL.'
            )
            Write-Verbose -Message $txt
        } #end if

        # Return to home drive
        try {

            Set-Location -Path $env:HOMEDRIVE\ -ErrorAction Stop

        } catch {

            Write-Warning -Message ('Failed to return to home drive: {0}' -f $_.Exception.Message)

        } #end try-catch

        # If results are available and not being piped elsewhere, format them for display
        if ($result.Count -gt 0) {
            # Only display formatted output if not being used in a pipeline
            if (-not $MyInvocation.ExpectingInput -and [System.Console]::IsOutputRedirected -eq $false) {
                # Output the summary information from StringBuilder
                Write-Output $sb.ToString()

                # Check if terminal supports ANSI colors
                $supportsAnsi = $Host.UI.SupportsVirtualTerminal -or ($env:TERM -like '*xterm*') -or ($env:ConEmuANSI -eq 'ON')

                # If ANSI is supported, use colors, otherwise use plain text
                if ($supportsAnsi) {
                    # Create a more readable and color-coded output
                    $Splat = @{
                        InputObject = $result
                        Property    = @(
                            @{Name = 'ACE #'; Expression = { $_.ACENumber } },
                            @{Name = 'Identity'; Expression = { "$blue$($_.Id)$reset" } },
                            @{Name = 'Rights'; Expression = { "$green$($_.AdRight)$reset" } },
                            @{Name = 'Type'; Expression = {
                                    if ($_.AccessControlType -eq 'Allow') {
                                        "${green}Allow$reset"
                                    } else {
                                        "${red}Deny$reset"
                                    }
                                }
                            },
                            @{Name = 'ObjectType'; Expression = { "$cyan$($_.ObjectType)$reset" } },
                            @{Name = 'Inheritance'; Expression = { "$yellow$($_.AdSecurityInheritance)$reset" } },
                            @{Name = 'InheritedObjectType'; Expression = { "$purple$($_.InheritedObjectType)$reset" } },
                            @{Name = 'Inherited'; Expression = {
                                    if ($_.IsInherited) {
                                        "${green}Yes$reset"
                                    } else {
                                        "${gray}No$reset"
                                    }
                                }
                            }
                        )
                    } #end Splat
                } else {
                    # Plain text output for terminals without ANSI support
                    $Splat = @{
                        InputObject = $result
                        Property    = @(
                            @{Name = 'ACE #'; Expression = { $_.ACENumber } },
                            @{Name = 'Identity'; Expression = { $_.Id } },
                            @{Name = 'Rights'; Expression = { $_.AdRight } },
                            @{Name = 'Type'; Expression = { $_.AccessControlType } },
                            @{Name = 'ObjectType'; Expression = { $_.ObjectType } },
                            @{Name = 'Inheritance'; Expression = { $_.AdSecurityInheritance } },
                            @{Name = 'InheritedObjectType'; Expression = { $_.InheritedObjectType } },
                            @{Name = 'Inherited'; Expression = { $_.IsInherited } }
                        )
                    } #end Splat
                }

                try {
                    Format-Table @Splat -AutoSize -Wrap
                } catch {
                    Write-Warning -Message "Error formatting output table: $($_.Exception.Message)"
                    # Fallback to simple display
                    $result | Format-Table -AutoSize
                } #end try-catch
            }

            # Always return the result objects for pipeline or assignment
            return $result
        } else {
            # Return empty array if no results found
            Write-Verbose -Message 'No ACE entries were found or matched the criteria'
            return $result
        } #end If-Else
    } #end End
} #end Function Get-AclAccessRule