Report on Active Directory OU permissions using PowerShell

For one of our customers, we are working on restricting permissions of admin accounts by implementing Role Based Access and delegating permissions to Organizational Units (OU’s). But one of the first questions was… What are the current permissions and what should we remove and where? In this blog post I will show you a way to report on the current permissions so that you can remove them where they shouldn’t be granted 🙂


The script should be able to scan all OU’s in the Active Directory Domain, but you should also be able to specify a certain OU to start in including all child OU’s. The output should be stored in a CSV file for easy import in Excel or other tools which could report on the data. Things like inheritance should be reported and on what type of objects the permissions were given. (For example when Delegate Control was used for Helpdesk tasks like reset/unlock account etc.)


During the creation and testing of the Get-ActiveDirectoryOupermissions function, I ran into a few issues:

  • Some ObjectTypes were not translated to a friendly/more readable name, luckily someone already wrote a function for that which I used in the script (Downloaded that from here)
  • Some Built-In Security Identifiers were also not translated, made a translation for those using a Microsoft Docs page as input (here)

Running the script

After running the script, the Get-ActiveDirectoryOUpermissions function is available with two Parameters:

  • Output, enter the path to where the CSV file should be stored, e.g c:\temp\OU_ACL.csv
  • StartOU, the start OU to scan including child OU’s, format it like ‘OU=Servers,DC=domain,DC=Local

In the example below, I ran the Function with the -Output c:\temp\permissions.csv Parameter

When the script is done, it will tell you how many permissions were exported and where:

The permission.csv looks like this in Excel:

The script

Below is the script, you should run it as Administrator on a Domain-Joined computer with the ActiveDirectory Module installed or on a Domain Controller 😉

function Get-ActiveDirectoryOUpermissions {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Enter the path to where the CSV file should be stored, e.g c:\temp\OU_ACL.csv")][string]$Output,
        [Parameter(Mandatory = $false, HelpMessage = "Start OU to scan including child OU's, format it like 'OU=Servers,DC=domain,DC=Local'")][string]$StartOU

    #Validate output by creating the file, stop if location is inaccessible
    try {
        New-Item -Path $Output -ItemType File -Force:$true -ErrorAction Stop | Out-Null
        Write-Host ("Output to {0} is valid" -f $Output) -ForegroundColor Green
    catch {
        Write-Warning ("The output can't be saved as {0}, is specified path accessible?" -f $Output)

    #Try to detect the Active Directory Domain name before continuing and stop script if it fails
    try {
        $domain = (Get-ADDomain -ErrorAction Stop).DNSroot
        Write-Host ('Domain {0} detected' -f $domain) -ForegroundColor Green
    catch {
        Write-Warning "Could not retrieve Domain Name, is the ActiveDirectory module installed or are you running this from a non-domain-joined device?"

    #Continu if Active Directory Domain was detected and retrieve list of OU's from the whole domain
    #or from the Ou specified in the StartOU parameter
    if ($domain) {
        if ($StartOU) {
            try {
                Write-Host ("Retrieving OU's for Domain {0} starting from {1}" -f $domain, $StartOU) -ForegroundColor Green
                $oulist = Get-ADOrganizationalUnit -SearchBase $StartOU -Filter * -ResultSetSize 10000 -SearchScope Subtree -ErrorAction Stop | Sort-Object DistinguishedName
            catch {
                Write-Warning ("Could not use {0}, check spelling and format it like 'OU=Servers,DC=domain,DC=Local')" -f $startou)
        else {
            Write-Host ("Retrieving all OU's for Domain {0}" -f $domain, $StartOU) -ForegroundColor Green
            $oulist = Get-ADOrganizationalUnit -Filter * -ResultSetSize 10000 -SearchScope Subtree -ErrorAction Stop | Sort-Object DistinguishedName
    #Function for translating ObjectTypes to name
    #Thanks go out for the blog here
    function Get-NameForGUID {
        Begin {
            $DomainDC = ([ADSI]"").distinguishedName
            $ExtendedRightGUIDs = "LDAP://cn=Extended-Rights,cn=configuration,$DomainDC"
            $PropertyGUIDs = "LDAP://cn=schema,cn=configuration,$DomainDC"
        Process {
            If ($guid -eq "00000000-0000-0000-0000-000000000000") {
                Return "All"
            Else {
                $rightsGuid = $guid
                $property = "cn"
                $SearchAdsi = ([ADSISEARCHER]"(rightsGuid=$rightsGuid)")
                $SearchAdsi.SearchRoot = $ExtendedRightGUIDs
                $SearchAdsi.SearchScope = "OneLevel"
                $SearchAdsiRes = $SearchAdsi.FindOne()
                If ($SearchAdsiRes) {
                    Return $SearchAdsiRes.Properties[$property]
                Else {
                    $SchemaGuid = $guid
                    $SchemaByteString = "\" + ((([guid]$SchemaGuid).ToByteArray() | ForEach-Object { $_.ToString("x2") }) -Join "\")
                    $property = "ldapDisplayName"
                    $SearchAdsi = ([ADSISEARCHER]"(schemaIDGUID=$SchemaByteString)")
                    $SearchAdsi.SearchRoot = $PropertyGUIDs
                    $SearchAdsi.SearchScope = "OneLevel"
                    $SearchAdsiRes = $SearchAdsi.FindOne()
                    If ($SearchAdsiRes) {
                        Return $SearchAdsiRes.Properties[$property]
                    Else {
                        Return $guid.ToString()

    #Custom object for certain Security Identifiers which don't report a friendly name
    #List is from
    $customidentifiers = [PSCustomObject]@{
        'S-1-5-32-544' = 'Administrators'
        'S-1-5-32-545' = 'Users'
        'S-1-5-32-546' = 'Guests'
        'S-1-5-32-547' = 'Power Users'
        'S-1-5-32-548' = 'Account Operators'
        'S-1-5-32-549' = 'Server Operators'
        'S-1-5-32-550' = 'Print Operators'
        'S-1-5-32-551' = 'Backup Operators'
        'S-1-5-32-552' = 'Replicators'
        'S-1-5-32-554' = 'Builtin\Pre-Windows 2000 Compatible Access'
        'S-1-5-32-555' = 'Builtin\Remote Desktop Users'
        'S-1-5-32-556' = 'Builtin\Network Configuration Operators'
        'S-1-5-32-557' = 'Builtin\Incoming Forest Trust Builders'
        'S-1-5-32-558' = 'Builtin\Performance Monitor Users'
        'S-1-5-32-559' = 'Builtin\Performance Log Users'
        'S-1-5-32-560' = 'Builtin\Windows Authorization Access Group'
        'S-1-5-32-561' = 'Builtin\Terminal Server License Servers'
        'S-1-5-32-562' = 'Builtin\Distributed COM Users'
        'S-1-5-32-568' = 'Builtin\IIS_IUSRS'
        'S-1-5-32-569' = 'Builtin\Cryptographic Operators'
        'S-1-5-32-573' = 'Builtin\Event Log Readers'
        'S-1-5-32-574' = 'Builtin\Certificate Service DCOM Access'
        'S-1-5-32-575' = 'Builtin\RDS Remote Access Servers'
        'S-1-5-32-576' = 'Builtin\RDS Endpoint Servers'
        'S-1-5-32-577' = 'Builtin\RDS Management Servers'
        'S-1-5-32-578' = 'Builtin\Hyper-V Administrators'
        'S-1-5-32-579' = 'Builtin\Access Control Assistance Operators'
        'S-1-5-32-580' = 'Builtin\Remote Management Users'

    #Create empty variable acltotal, loop through all OU's and save the ACL's to $acltotal
    $acltotal = @()
    foreach ($ou in $oulist) {
        Write-Host ("Processing {0}" -f $ou.DistinguishedName) -ForegroundColor Green
        $acls = (Get-Acl -path "AD:$($ou.DistinguishedName)").Access
        foreach ($acl in $acls) {            
            #If IdentityReference matches item in $customidentifiers, change it to the friendly name
            #Otherwise just use the IdentityReference found by Get-Acl
            if ($customidentifiers | Select-string "$($acl.IdentityReference.Value)" -SimpleMatch ) {
                $IdentityReference = ($customidentifiers | Select-Object -Property $acl.IdentityReference.Value).$($acl.IdentityReference.Value)
            else {
                $IdentityReference = "$($acl.IdentityReference)"

            Write-Host ("- Retrieving {0} details for {1}" -f $acl.ActiveDirectoryRights, $IdentityReference) -ForegroundColor Gray

            $foundacls = [PSCustomObject]@{
                OrganizationalUnit = $ou.DistinguishedName
                Principal          = $IdentityReference
                Rights             = $acl.ActiveDirectoryRights
                AppliesTo          = Get-NameForGUID $acl.InheritedObjectType
                Item               = Get-NameForGUID $acl.ObjectType
                Access             = $acl.AccessControlType
                Inheritance        = $acl.InheritanceType
                InheritanceFrom    = $acl.InheritanceFlags
            $acltotal += $foundacls

    #Export results to CSV file
    if ($acltotal.count -gt 0) {
        Write-Host ("Exporting {0} results to {1}" -f $acltotal.count, $Output) -ForegroundColor Green
        $acltotal | Sort-Object OrganizationalUnit, Principal, Rights, AppliesTo, Item, Access, Inheritance, InheritanceFrom | Export-Csv -Path $Output -Encoding UTF8 -Delimiter ';' -NoTypeInformation

Download the script(s) from GitHub here

2 thoughts on “Report on Active Directory OU permissions using PowerShell

  1. I’ve been using Ashley McGlone’s OUPermissions script for several years. Both that script (which I subsequently modified) and yours overlooks the other containers in Active Directory that administrators may also need to review permissions for.

    • Yes, containers like Computers or Users are not taken into the report. The script only does OU’s, I could update it in the future to also include containers

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.