Short answer: run .\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\acl.csv. You get one CSV row per permission entry on every folder down to three levels: who (DOMAIN\name and SID), which rights, allow or deny, inherited or explicit, and what it applies to. Add -ExplicitOnly to see only the places where someone changed permissions by hand. Folders it cannot read are listed with the error instead of stopping the scan.
We ran this script on 6 October 2026 on a Windows Server 2025 Standard domain controller (build 26100.33438, September 2026 update, Windows PowerShell 5.1) and a Windows 11 Pro member PC in our lab domain contoso.com. The example output below is from that run; the lab domain is small and new, so your numbers will be bigger. It was also syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.
Table of Contents
What it does
File server audits always ask the same question: who can get into which folder? Opening Advanced Security on hundreds of folders is not an answer, and (Get-Acl).Access | Export-Csv breaks on the first access-denied folder. This read-only script walks the tree and flattens the ACLs into a table you can filter in Excel.
- One row per access rule (ACE) with Identity, SID, Rights, AccessType (Allow/Deny), IsInherited and AppliesTo in plain words (“This folder, subfolders and files”, “Subfolders only” …).
- Owner and inheritance state for every folder;
InheritanceBlockedis True where inheritance was turned off, which is where explicit permissions usually live. - SIDs resolved once and cached. Entries for deleted accounts show as “Unresolved SID (S-1-5-21-…)”, a clean-up candidate.
- Generic rights decoded. Inherit-only entries such as CREATOR OWNER often carry generic rights that show up as large numbers; the script names them (GenericAll, GenericRead …).
- Errors become rows. Access denied and similar problems are written to the Error column and the scan continues.
- Filters:
-Depth,-IncludeFiles,-ExplicitOnlyand-Identity(wildcards, e.g. “*Everyone*”).
For the rules behind a clean permission design (share vs NTFS permissions, groups instead of users, where to break inheritance) see NTFS permissions and share permissions.
Requirements
- Windows PowerShell 5.1 or PowerShell 7 on Windows. No extra modules: it uses
Get-ChildItemandGet-Acl. - Read permission on the folders and their ACLs. Run it as an account that can read every folder, usually a local administrator on the file server; otherwise locked-down folders appear as error rows.
- For SID-to-name translation, the machine must be able to reach a domain controller.
- Local paths (D:\Shares) or UNC paths (\\fs01\Finance) both work. Running it on the file server itself is faster than over the network.
Download and first run
- Copy the script from the box on this page (or use the download button) and save it as
C:\Scripts\Get-NTFSPermissionReport.ps1. - If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run:
Unblock-File C:\Scripts\Get-NTFSPermissionReport.ps1. - Read the built-in help once, then run it with no options to see the result on screen before you export anything.
cd C:\Scripts
Get-Help .\Get-NTFSPermissionReport.ps1 -Full
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 1
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\acl.csv
Options
| Parameter | What it does | Default |
|---|---|---|
-Path | Root folder (local or UNC) | Required |
-Depth | Levels below -Path to scan (0 = root only, max 100) | 2 |
-IncludeFiles | Report files too, not only folders | Off |
-ExplicitOnly | Only non-inherited entries (the root folder is always reported in full) | Off |
-Identity | Only entries whose identity matches this wildcard | All |
-ExportCsv | Write a CSV file | Not written |
-PassThru | Send the row objects down the pipeline | Off |
Usage examples
# Three levels of a share root to CSV
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\shares-acl.csv
# Where were permissions set by hand? (deep scan, explicit entries only)
.\Get-NTFSPermissionReport.ps1 -Path \\fs01\Finance -Depth 10 -ExplicitOnly -ExportCsv C:\Reports\finance-explicit.csv
# Any folder that grants Everyone or Authenticated Users something
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 5 -PassThru |
Where-Object { $_.Identity -match 'Everyone|Authenticated Users' } | Format-Table Path, Identity, Rights
# Direct user permissions (not groups) - often a sign of quick fixes
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 5 -ExplicitOnly -Identity "CONTOSO\*" -PassThru |
Where-Object { -not $_.IsInherited } | Sort-Object Identity
# Deleted accounts still in ACLs
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 5 -Identity "Unresolved SID*" -ExportCsv C:\Reports\orphan-sids.csv
The -Identity "CONTOSO\*" filter matches both users and groups of the domain; to tell them apart, join the CSV with an AD export or check a few names. The report does not query AD for object types, to stay fast on large trees.
CSV columns
The example output further down is from our lab run. The columns are:
| Column | Meaning |
|---|---|
| Path, ItemType | Full path; Folder or File |
| Owner | Owner of the item as reported by Get-Acl |
| InheritanceBlocked | True if inheritance from the parent is disabled on this item |
| Identity, Sid | Account or group (DOMAIN\name) and its SID |
| Rights | FileSystemRights, e.g. “Modify, Synchronize”, “ReadAndExecute, Synchronize”, “FullControl” |
| AccessType | Allow or Deny |
| IsInherited | True if the entry comes from a parent folder |
| AppliesTo | Plain-language scope from the inheritance and propagation flags |
| InheritanceFlags, PropagationFlags | The raw .NET flags, for anyone who wants them |
| Error | Empty, or why the item could not be read or listed |
Example output
Run against a small test share with -Path C:\Shares -Depth 2 -ExportCsv C:\srvs-lab\out\ntfs.csv. Console line and the first rows of the CSV:
30 row(s) from 4 item(s) written to C:\srvs-lab\out\ntfs.csv; 0 item(s) could not be read.
Path Identity Rights AccessType IsInherited AppliesTo
---- -------- ------ ---------- ----------- ---------
C:\Shares BUILTIN\Administrators FullControl Allow False This folder only
C:\Shares NT AUTHORITY\SYSTEM FullControl Allow True This folder, subfolders and files
C:\Shares BUILTIN\Users ReadAndExecute, Synchronize Allow True This folder, subfolders and files
C:\Shares CREATOR OWNER GenericAll Allow True Subfolders and files only
Schedule it
A monthly or weekly snapshot gives you a history of who had access when, which auditors like. Run it on the file server itself, outside working hours on big trees. The gMSA it runs as needs read access to every folder; see our gMSA guide.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\shares-acl.csv'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 2am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'NTFS permission report' -Action $action -Trigger $trigger -Principal $principal
The file is overwritten on every run. If you need history, point the task at a small wrapper script that adds the date to the file name, or copy the file away after each run.
How it works
Get-ChildItem -LiteralPath <Path> -Recurse -Depth (Depth-1) -Directory -Forcelists the folders (add files with-IncludeFiles). Listing errors are collected with-ErrorVariableinstead of stopping the run.- For each item
Get-Acl -LiteralPathreads the security descriptor. A failure becomes an error row. GetAccessRules($true, $true, [SecurityIdentifier])returns explicit and inherited rules with raw SIDs, which avoids slow or failing translation inside the .NET call. Each SID is translated once to an NT account name and cached.- The inheritance and propagation flags are mapped to the same “Applies to” wording you see in the Advanced Security dialog. Generic access bits are named separately.
- Rows are exported or returned.
Limitations
- NTFS only. Share permissions (the SMB share ACL) are not included; read those with
Get-SmbShareAccesson the file server. - Long paths. Windows PowerShell 5.1 can fail on paths longer than 260 characters; those folders show up as error rows. PowerShell 7 is generally better with long paths, so try it there if you see many.
- Audit entries (SACL) are not reported; reading them needs extra privileges.
- Size. Every ACE is a row. A deep scan with
-IncludeFileson a large share can produce millions of rows; start with folders and-ExplicitOnly. - Effective access (group nesting, deny precedence, share permissions combined) is not calculated; the report shows the configured entries.
- Not yet run on a Windows file server (see the note at the top).
Official documentation: Get-Acl (Microsoft Learn) · FileSystemAccessRule class (.NET) · FileSystemRights enum (.NET)
Related: NTFS Permissions and Share Permissions: 7 Secure File Server Rules · Map Network Drives with Group Policy: Drive Maps, Targeting and Fixes · Folder Redirection Group Policy: 7 Steps for Windows 11 · Get-ADUser PowerShell Examples: 25 Queries for Active Directory
See also: Export AD Users to CSV: PowerShell Script with Last Logon · Locked Out AD Users Report: PowerShell Script with Lockout Source · AD Privileged Group Report: Domain Admins and adminCount Audit · AD Nested Group Membership: PowerShell Tree with Loop Detection · Inactive AD Accounts Report: PowerShell Script with Safe Disable
The script
# NTFS Permissions Report to CSV: PowerShell Folder ACL Script (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/ntfs-permissions-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
Export the NTFS permissions (ACLs) of a folder tree to CSV: who has what, explicit or inherited.
.DESCRIPTION
Read-only. Walks a folder (local path or UNC share) to -Depth levels with Get-ChildItem, reads each ACL
with Get-Acl and writes one row per access rule (ACE):
Path, ItemType, Owner, InheritanceBlocked, Identity, Sid, Rights, AccessType, IsInherited,
AppliesTo, InheritanceFlags, PropagationFlags, Error
SIDs are translated to DOMAIN\name once each (cached). A SID that no longer resolves (deleted account)
is reported as "Unresolved SID", which is usually worth cleaning up.
Generic rights that appear on inherit-only entries (for example CREATOR OWNER) are shown by name
(GenericAll, GenericRead ...) instead of a bare number.
Folders that cannot be read (access denied, path too long) get a row with the Error column filled and
the scan carries on.
.PARAMETER Path
Root folder to scan, e.g. D:\Shares\Finance or \\fs01\Finance.
.PARAMETER Depth
How many levels below -Path to scan (default 2; 0 = only the root folder).
.PARAMETER IncludeFiles
Also report files (default: folders only). On large trees this multiplies the run time.
.PARAMETER ExplicitOnly
Report only explicit (non-inherited) entries, plus the root folder in full. This is the quickest way to
find where permissions were changed by hand.
.PARAMETER Identity
Only report entries for identities matching this wildcard, e.g. "*Finance*" or "CONTOSO\bob".
.PARAMETER ExportCsv
Write the report to this CSV file (UTF-8).
.PARAMETER PassThru
Output the row objects to the pipeline.
.EXAMPLE
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\shares-acl.csv
.EXAMPLE
.\Get-NTFSPermissionReport.ps1 -Path \\fs01\Finance -ExplicitOnly -Depth 10 -ExportCsv C:\Reports\finance-explicit.csv
.EXAMPLE
.\Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 5 -Identity "*Everyone*" -PassThru | Format-Table Path, Rights
.NOTES
Name: Get-NTFSPermissionReport.ps1
Version: 1.0.0
Source: https://srvscripts.com/scripts/ntfs-permissions-report/
License: MIT
Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, read permission on the folders (run as an
account that can read every ACL, e.g. a member of Administrators on the file server).
No extra modules.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[ValidateRange(0, 100)]
[int]$Depth = 2,
[switch]$IncludeFiles,
[switch]$ExplicitOnly,
[ValidateNotNullOrEmpty()]
[string]$Identity,
[ValidateNotNullOrEmpty()]
[string]$ExportCsv,
[switch]$PassThru
)
Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
throw "Folder not found or not accessible: $Path"
}
$rootItem = Get-Item -LiteralPath $Path
$sidCache = @{}
function Resolve-Sid {
param([System.Security.Principal.SecurityIdentifier]$Sid)
$k = $Sid.Value
if (-not $sidCache.ContainsKey($k)) {
try { $sidCache[$k] = $Sid.Translate([System.Security.Principal.NTAccount]).Value }
catch { $sidCache[$k] = "Unresolved SID ($k)" }
}
$sidCache[$k]
}
function Format-Right {
param($Rights)
$v = [int64]([int]$Rights) -band 0xFFFFFFFFL
$generic = @()
if ($v -band 0x10000000L) { $generic += 'GenericAll' }
if ($v -band 0x20000000L) { $generic += 'GenericExecute' }
if ($v -band 0x40000000L) { $generic += 'GenericWrite' }
if ($v -band 0x80000000L) { $generic += 'GenericRead' }
$specific = $v -band 0x0FFFFFFFL
$parts = @()
if ($specific) { $parts += [Enum]::ToObject([System.Security.AccessControl.FileSystemRights], [int]$specific).ToString() }
($parts + $generic) -join ', '
}
function Get-AppliesTo {
param([System.Security.AccessControl.InheritanceFlags]$Inherit,
[System.Security.AccessControl.PropagationFlags]$Propagate,
[bool]$IsContainer)
if (-not $IsContainer) { return 'This file' }
$ci = [bool]($Inherit -band [System.Security.AccessControl.InheritanceFlags]::ContainerInherit)
$oi = [bool]($Inherit -band [System.Security.AccessControl.InheritanceFlags]::ObjectInherit)
$io = [bool]($Propagate -band [System.Security.AccessControl.PropagationFlags]::InheritOnly)
$np = [bool]($Propagate -band [System.Security.AccessControl.PropagationFlags]::NoPropagateInherit)
$text = if (-not $ci -and -not $oi) { 'This folder only' }
elseif ($ci -and $oi -and -not $io) { 'This folder, subfolders and files' }
elseif ($ci -and -not $oi -and -not $io) { 'This folder and subfolders' }
elseif ($oi -and -not $ci -and -not $io) { 'This folder and files' }
elseif ($ci -and $oi -and $io) { 'Subfolders and files only' }
elseif ($ci -and $io) { 'Subfolders only' }
else { 'Files only' }
if ($np -and ($ci -or $oi)) { $text += ' (one level only)' }
$text
}
function Get-ErrorRow {
param([string]$ItemPath, [string]$Type, [string]$Message)
[pscustomobject]@{
Path = $ItemPath; ItemType = $Type; Owner = ''; InheritanceBlocked = $null; Identity = ''; Sid = ''
Rights = ''; AccessType = ''; IsInherited = $null; AppliesTo = ''; InheritanceFlags = ''; PropagationFlags = ''
Error = $Message
}
}
# ---- Collect items --------------------------------------------------------------------------------------
$items = [System.Collections.Generic.List[object]]::new()
$items.Add($rootItem)
$scanErrors = @()
if ($Depth -gt 0) {
$gci = @{ LiteralPath = $rootItem.FullName; Recurse = $true; Depth = ($Depth - 1); Force = $true;
ErrorAction = 'SilentlyContinue'; ErrorVariable = 'scanErrors' }
if (-not $IncludeFiles) { $gci.Directory = $true }
foreach ($i in (Get-ChildItem @gci)) { $items.Add($i) }
}
Write-Verbose ("{0} item(s) to read" -f $items.Count)
# ---- Read ACLs ------------------------------------------------------------------------------------------
$rows = [System.Collections.Generic.List[object]]::new()
foreach ($e in $scanErrors) {
$target = [string]$e.TargetObject
if (-not $target) { $target = $Path }
$rows.Add((Get-ErrorRow -ItemPath $target -Type 'Folder' -Message ("Not listed: " + $e.Exception.Message)))
}
$n = 0
foreach ($item in $items) {
$n++
if ($n % 200 -eq 0) { Write-Progress -Activity 'Reading ACLs' -Status $item.FullName -PercentComplete (100 * $n / $items.Count) }
$isDir = $item.PSIsContainer
$type = if ($isDir) { 'Folder' } else { 'File' }
try {
$acl = Get-Acl -LiteralPath $item.FullName
} catch {
$rows.Add((Get-ErrorRow -ItemPath $item.FullName -Type $type -Message $_.Exception.Message))
continue
}
$owner = ''
try { $owner = $acl.Owner } catch { $owner = 'Unknown' }
$isRoot = ($item.FullName -eq $rootItem.FullName)
$rules = $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
foreach ($r in $rules) {
if ($ExplicitOnly -and $r.IsInherited -and -not $isRoot) { continue }
$name = Resolve-Sid $r.IdentityReference
if ($Identity -and ($name -notlike $Identity)) { continue }
$rows.Add([pscustomobject]@{
Path = $item.FullName
ItemType = $type
Owner = $owner
InheritanceBlocked = $acl.AreAccessRulesProtected
Identity = $name
Sid = $r.IdentityReference.Value
Rights = Format-Right $r.FileSystemRights
AccessType = [string]$r.AccessControlType
IsInherited = $r.IsInherited
AppliesTo = Get-AppliesTo $r.InheritanceFlags $r.PropagationFlags $isDir
InheritanceFlags = [string]$r.InheritanceFlags
PropagationFlags = [string]$r.PropagationFlags
Error = ''
})
}
}
Write-Progress -Activity 'Reading ACLs' -Completed
$out = @($rows)
# ---- Output ---------------------------------------------------------------------------------------------
$errCount = @($out | Where-Object { $_.Error }).Count
if ($ExportCsv) {
$out | Export-Csv -LiteralPath $ExportCsv -NoTypeInformation -Encoding UTF8
Write-Information ("{0} row(s) from {1} item(s) written to {2}; {3} item(s) could not be read." -f $out.Count, $items.Count, $ExportCsv, $errCount) -InformationAction Continue
}
if ($PassThru) { return $out }
if (-not $ExportCsv) {
$out | Format-Table Path, Identity, Rights, AccessType, IsInherited, AppliesTo, Error -AutoSize -Wrap
}
4e878256b338c9bfa891130909de3b488af0a594e69b49adf8f41ac986ffefc2curl -fsSL -o Get-NTFSPermissionReport.ps1 https://scr.srvscripts.com/ntfs-permissions-report/Get-NTFSPermissionReport.ps1 && curl -fsSL https://scr.srvscripts.com/ntfs-permissions-report/Get-NTFSPermissionReport.ps1.sha256 | sha256sum -cInvoke-WebRequest -Uri 'https://scr.srvscripts.com/ntfs-permissions-report/Get-NTFSPermissionReport.ps1' -OutFile 'Get-NTFSPermissionReport.ps1'; if ((Get-FileHash 'Get-NTFSPermissionReport.ps1' -Algorithm SHA256).Hash -eq '4E878256B338C9BFA891130909DE3B488AF0A594E69B49ADF8F41AC986FFEFC2') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.Also on GitHub: github.com/srvscripts/scripts
Frequently asked questions
How do I export NTFS folder permissions to CSV with PowerShell?
Run Get-NTFSPermissionReport.ps1 -Path D:\Shares -Depth 3 -ExportCsv C:\Reports\acl.csv. It writes one row per permission entry with identity, rights, allow or deny, inherited or explicit, and scope.
How do I find folders with broken inheritance?
Filter the CSV on InheritanceBlocked = True, or run the script with -ExplicitOnly to list only entries that were set directly on a folder.
What does “Unresolved SID” mean?
The permission belongs to an account or group that no longer exists, or that this machine cannot look up. Deleted accounts in ACLs are safe to remove once you have confirmed the SID is not from a trusted domain.
Why does a right show as GenericAll or GenericRead?
Inherit-only entries such as CREATOR OWNER often use generic rights, which Windows maps to specific rights on new child objects. GenericAll corresponds to Full control.
Does the script include share permissions?
No. It reports NTFS permissions only. Use Get-SmbShareAccess -Name ShareName on the file server for the share ACL.
Will it change any permissions?
No. It only reads ACLs with Get-Acl and never calls Set-Acl.