PowerShell NTFS Permission Audit: Find Broken Inheritance

PowerShell NTFS Permission Audit: Find Broken Inheritance

PowerShell Tips Editor 4 min read
PowerShell NTFS Permission Audit: Find Broken Inheritance

How NTFS Permission Sprawl Happens

Every “quick exception” someone makes — breaking inheritance on a project folder, adding an explicit ACE for a contractor, giving a service account broad access just this once — accumulates silently. File servers that have been in production for several years routinely contain hundreds of folders where inheritance was broken and explicit permissions were never cleaned up. No one audits them until a security incident or compliance review forces the issue. PowerShell’s Get-Acl and ACE introspection properties let you scan an entire share, identify every broken inheritance point, flag overly permissive entries, and export a risk-ranked report in minutes.

Quick Answer

Use Get-Acl on each folder returned by Get-ChildItem -Recurse, check .Access.IsInherited to find explicit ACEs, and check .AreAccessRulesProtected to identify folders where inheritance is broken at the container level.

Getting ACLs with Get-Acl and Inspecting ACE Properties

Each object returned by Get-Acl has an Access property — a collection of FileSystemAccessRule objects. Key properties on each rule are IdentityReference, FileSystemRights, AccessControlType, and IsInherited. The IsInherited property distinguishes ACEs that came from a parent folder from ones that were explicitly set on this object.

$acl = Get-Acl -Path 'C:\FileShare\ProjectData'

$acl.Access | Select-Object IdentityReference,
                             FileSystemRights,
                             AccessControlType,
                             IsInherited |
    Format-Table -AutoSize
IdentityReference               FileSystemRights  AccessControlType IsInherited
-----------------               ----------------  ----------------- -----------
BUILTIN\Administrators          FullControl       Allow             True
DOMAIN\ProjectTeam              Modify            Allow             True
DOMAIN\contractor.jane          FullControl       Allow             False
NT AUTHORITY\Authenticated Users Modify           Allow             False

The two IsInherited = False rows are explicit ACEs — they were set directly on this folder and will survive if the parent ACL changes.

Detecting Inheritance Protection with AreAccessRulesProtected

Broken inheritance means the folder no longer receives ACE changes from its parent. The AreAccessRulesProtected property on the ACL object is a Boolean that is $true whenever inheritance has been disabled for that folder.

function Test-InheritanceBroken {
    param([string]$FolderPath)
    try {
        $acl = Get-Acl -Path $FolderPath -ErrorAction Stop
        [PSCustomObject]@{
            Path                   = $FolderPath
            InheritanceBroken      = $acl.AreAccessRulesProtected
            ExplicitAceCount       = ($acl.Access | Where-Object { -not $_.IsInherited }).Count
            TotalAceCount          = $acl.Access.Count
        }
    }
    catch {
        Write-Warning "Access denied reading ACL: $FolderPath"
        $null
    }
}

Test-InheritanceBroken -FolderPath 'C:\FileShare\ProjectData'

Finding Explicit ACEs That Override Inheritance

Filtering on IsInherited -eq $false isolates every ACE that was placed directly on the object. These are the entries worth reviewing — they represent deliberate (or forgotten) exceptions to the standard permission model.

function Get-ExplicitAces {
    param([string]$FolderPath)
    try {
        $acl = Get-Acl -Path $FolderPath -ErrorAction Stop
        $acl.Access |
            Where-Object { -not $_.IsInherited } |
            Select-Object @{N='Path';E={$FolderPath}},
                          IdentityReference,
                          FileSystemRights,
                          AccessControlType
    }
    catch { $null }
}

Get-ExplicitAces -FolderPath 'C:\FileShare\ProjectData'

Flagging Overly Permissive ACEs for Everyone or Authenticated Users

ACEs granting Modify or FullControl to Everyone, NT AUTHORITY\Authenticated Users, or BUILTIN\Users are the highest risk findings. These identities cover essentially all domain users, making the permission functionally public within the organization.

$riskyIdentities = @(
    'Everyone',
    'NT AUTHORITY\Authenticated Users',
    'BUILTIN\Users'
)
$riskyRights = @('FullControl', 'Modify', 'Write')

function Test-OverlyPermissive {
    param([System.Security.AccessControl.FileSystemSecurity]$Acl, [string]$Path)
    foreach ($ace in $Acl.Access) {
        $identity = $ace.IdentityReference.Value
        $rights   = $ace.FileSystemRights.ToString()
        if ($identity -in $riskyIdentities -and
            ($riskyRights | Where-Object { $rights -match $_ })) {
            [PSCustomObject]@{
                Path             = $Path
                Identity         = $identity
                Rights           = $rights
                IsInherited      = $ace.IsInherited
                RiskLevel        = 'High'
            }
        }
    }
}

Recursive Scan Across Deep Directory Trees Efficiently

Combine all the checks above into a single recursive scan. Use -ErrorAction SilentlyContinue on Get-ChildItem to skip inaccessible folders rather than aborting the entire scan.

$scanRoot  = 'C:\FileShare'
$findings  = [System.Collections.Generic.List[PSCustomObject]]::new()

$folders = Get-ChildItem -Path $scanRoot -Recurse -Directory -ErrorAction SilentlyContinue

foreach ($folder in $folders) {
    try {
        $acl = Get-Acl -Path $folder.FullName -ErrorAction Stop
        if ($acl.AreAccessRulesProtected) {
            $findings.Add([PSCustomObject]@{
                Path       = $folder.FullName
                FindingType= 'BrokenInheritance'
                Detail     = "Inheritance disabled; $($acl.Access.Count) ACEs present"
                RiskLevel  = 'Medium'
            })
        }
        foreach ($result in (Test-OverlyPermissive -Acl $acl -Path $folder.FullName)) {
            $findings.Add($result)
        }
    }
    catch { Write-Warning "Skipped (access denied): $($folder.FullName)" }
}

Write-Host "Scan complete. $($findings.Count) findings across $($folders.Count) folders"

Exporting Findings to CSV with Risk Rating

Sort findings by risk level and path, then export to CSV for review or import into a ticketing system.

Common Errors

  • Get-Acl throws access denied on folders where the account lacks read permissions. Even a local administrator may hit this on folders with broken inheritance that removed admin access. Wrap every Get-Acl call in a try/catch and log inaccessible paths separately — they are often the most interesting findings.
  • Inherited ACEs appear in the ACL list — filter with the IsInherited property. $acl.Access returns both inherited and explicit ACEs. Without filtering on IsInherited -eq $false, your “explicit ACE” report will be flooded with inherited entries that are perfectly normal and expected.

Related Cmdlets / See Also

Wrapping Up

An NTFS permission audit built on Get-Acl, AreAccessRulesProtected, and IsInherited filtering turns a multi-day manual review into an automated scan that completes in minutes. Run it quarterly, compare findings between runs, and close the gap between your intended permission model and reality before the next compliance review.

Send-Item -To