Taking Inventory of a File Server with PowerShell — Capacity Analysis and Access Permission (ACL) Auditing

· · PowerShell, Windows, File Server, ACL, Access Permissions, Operational Improvement, Security, Script

“The file server is down to less than 10% free space.” Work that begins with that warning is usually depressing. Nobody knows what is eating the capacity. Nobody can judge whether a ten-year-old folder that no one has touched is safe to delete. And nobody can answer on the spot who has access to what, or whether permissions for people who left the company are still in place. Going around opening Properties in File Explorer is a survey that never finishes on a multi-terabyte share.

Every time we get our hands dirty with this kind of request, the same thought recurs: tidying a file server is not a “deletion skill,” it is an “inventory skill.” Ask “can we delete this?” without numbers and a list, and departments will never say yes. Present a table saying “this folder alone is 800 GB, and 620 GB of it hasn’t been modified in over three years,” and the conversation moves forward immediately.

This article is aimed at IT staff and operations people at small and medium-sized businesses, and lays out the practical procedure for taking inventory of a file server’s capacity and access permissions (ACLs) with PowerShell, all the way to a CSV report. The policy throughout is consistent: read first, change last, with backups and -WhatIf.

1. The Bottom Line First

  • Capacity analysis boils down to Get-ChildItem -Recurse + Measure-Object -Sum. Do not aggregate the whole tree at once; work per first-level folder to get a rough sense of where the bulk sits.12
  • Do not swallow access denials — record them. In exchange for not stopping the scan with -ErrorAction SilentlyContinue, always capture the denied locations with -ErrorVariable. A place you could not count is not “zero,” it is “unknown.”3
  • Base your “old file” judgement on LastWriteTime. In many environments NTFS last access time updates are disabled (or system-managed), so they cannot be trusted as evidence.45
  • Extract duplicate “candidates” with Get-FileHash (SHA256 by default). Narrowing by size before computing hashes is the standard way to save I/O.6
  • The ACL inventory is Get-Acl’s Access property. Put IdentityReference (who), FileSystemRights (what), and IsInherited (inherited or direct) into a CSV, and use AreAccessRulesProtected to detect where inheritance has been broken.78
  • What this article inventories is NTFS permissions. Effective access over a share (SMB) is determined by the combination of the share permissions and the NTFS ACL. List the share side separately with Get-SmbShareAccess and read it alongside the NTFS ledger.9
  • Specify the character encoding of your CSV reports explicitly. Windows PowerShell 5.1’s Export-Csv defaults to ASCII, which corrupts non-ASCII text, while PowerShell 7’s default is UTF-8 without BOM, which can interact badly with Excel’s default behaviour.1011
  • Only make changes (Set-Acl) once the inventory and departmental confirmation are done. Take a backup with icacls /save, confirm the targets with -WhatIf, and then apply.1213

2. Capacity Analysis — Turning “Where Is It Bloated?” Into Numbers

The first thing to do is get the total size of each folder directly under the share root. Enumerate files recursively with Get-ChildItem and total Length with Measure-Object.12

$root = 'D:\share'   # Assumes running on the file server. See Section 6 for cautions about running over UNC
$denied = @()

# Aggregate per first-level folder — getting a rough idea comes before totalling everything
# Apply the same options to the outer Get-ChildItem so failures enumerating the root land in $denied too
$report = foreach ($dir in Get-ChildItem -LiteralPath $root -Directory `
    -ErrorAction SilentlyContinue -ErrorVariable +denied) {
    # SilentlyContinue so access denials do not stop us, but denials still accumulate in $denied
    # Pipe the enumeration straight into Measure-Object (capturing it in a variable would keep every
    # FileInfo in memory, which becomes painful on folders with millions of files)
    $stats = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File `
        -ErrorAction SilentlyContinue -ErrorVariable +denied |
        Measure-Object -Property Length -Sum
    [PSCustomObject]@{
        Folder    = $dir.Name
        SizeGB    = [math]::Round([double]$stats.Sum / 1GB, 2)
        FileCount = $stats.Count
    }
}
# Count files placed directly in the root as one row too (they are missed by the per-folder aggregation)
$rootStats = Get-ChildItem -LiteralPath $root -File `
    -ErrorAction SilentlyContinue -ErrorVariable +denied |
    Measure-Object -Property Length -Sum
if ($rootStats.Count -gt 0) {
    $report += [PSCustomObject]@{
        Folder    = '(directly in root)'
        SizeGB    = [math]::Round([double]$rootStats.Sum / 1GB, 2)
        FileCount = $rootStats.Count
    }
}

$report | Sort-Object SizeGB -Descending | Format-Table -AutoSize

# Always keep a record of "what could not be counted" — if this is not empty, the totals are incomplete
$denied | ForEach-Object { $_.TargetObject } | Sort-Object -Unique |
    Set-Content -Path .\denied-paths.txt

There are two things to note. First, -ErrorAction SilentlyContinue is not a switch that means “pretend the error never happened.” It only suppresses the display and continues; the error still occurs.3 That is why you catch it with -ErrorVariable. Prefixing the variable name with + appends rather than overwrites, so you can collect the denials from the whole loop into a single variable.3 Folders you were denied access to are missing from the totals, so the report shows them as smaller than reality. If denied-paths.txt is not empty, be sure to say so in the report. If you want to go a level deeper on error-handling design, “PowerShell Error Handling and Retry Design,” published alongside this article, is a useful reference.

Second, the problem of deep hierarchies. A share that has been in use for years will almost certainly contain paths longer than 260 characters (MAX_PATH), and some tools fail to enumerate at that point.14 When your scan results show an unnatural gap, suspect path length first. The full picture of this limit and how to deal with it is collected in “MAX_PATH and Windows Path/Filename Pitfalls.”

3. Old Files and Duplicate Candidates — Start From a List, Not From Deletion

3.1. Listing “Files Untouched for Three Years”

Once you know where things are bloated, the next step is gathering material for “deletion candidates.” The criterion to use is LastWriteTime (last modified time).

$cutoff = (Get-Date).AddYears(-3)

# A list of files not modified in three years or more. Nothing is deleted — this is material to show the department first
# Record locations that failed to enumerate with -ErrorVariable so they can be disclosed later
Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable oldEnumErrors |
    Where-Object LastWriteTime -lt $cutoff |
    Select-Object FullName, LastWriteTime,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Length / 1MB, 2) } } |
    Sort-Object SizeMB -Descending |
    Export-Csv -Path .\old-files.csv -NoTypeInformation -Encoding utf8BOM   # Use UTF8 on 5.1 (both are BOM-prefixed)

# Always disclose the locations that could not be enumerated, to stop the list from looking complete
$oldEnumErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\old-files-uninspected.txt

It is tempting to think “if I use the last access time (LastAccessTime), I can even tell which files have not been read” — but that is a trap. Because updating last access times affects performance on NTFS, whether updates are enabled or disabled is controlled through the fsutil behavior command and the registry (NtfsDisableLastAccessUpdate),4 and since Windows Vista the default has been disabled (in recent Windows 10 and later it is system-managed, and on servers it is disabled).5 In other words, files that are actually being opened but whose timestamps stay old are perfectly normal. Use LastWriteTime — “the date the content was last changed” — in your inventory documentation, and state what it means explicitly; that keeps the conversation with the department from going sideways.

3.2. Raising Duplicate File “Candidates”

Shared folders accumulate piles of “final version,” “final version_revised,” and “Copy of …”. Whether the contents are identical can be determined with Get-FileHash. The default algorithm is SHA256, and if the hash values match you can conclude the file contents are identical.6

# Hashing every file is I/O heavy. Narrow down to "files of the same size" first
# Record locations that failed to enumerate (access denials and so on) with -ErrorVariable for later disclosure
$candidates = Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable enumErrors |
    Group-Object -Property Length |
    Where-Object { $_.Count -ge 2 -and [long]$_.Name -gt 0 } |   # Exclude zero-length files
    ForEach-Object { $_.Group }

# Hash only the narrowed candidates and output matching groups as duplicate "candidates"
$candidates |
    Get-FileHash -ErrorAction SilentlyContinue -ErrorVariable hashErrors |   # SHA256 by default
    Group-Object -Property Hash |
    Where-Object Count -ge 2 |
    ForEach-Object { $_.Group } |
    Select-Object Hash, Path |
    Export-Csv -Path .\duplicate-candidates.csv -NoTypeInformation -Encoding utf8BOM   # Use UTF8 on 5.1

# Always keep a list of folders that could not be enumerated, and files whose hash could not be
# computed because they were locked, unreadable, and so on, as "could not be inspected"
# (dropping them silently makes them indistinguishable from "no duplicates")
@($enumErrors) + @($hashErrors) | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\hash-uninspected.txt

They are deliberately called “candidates” because deciding which one to keep is a business question, not a technical one. Even with identical content, “department A’s master” and “department B’s reference copy” can mean different things. Do not delete mechanically — this script’s remit extends only as far as taking the list and discussing it with the people involved. The nature of the hashing tool itself (what it guarantees and what it does not) is covered in “A Practical Procedure for Identifying the Scheme Behind a Hash String.”

4. Auditing Access Permissions (ACLs) — Putting “Who Can Do What” Into a Table

The other inventory target, alongside capacity, is permissions. Get-Acl retrieves the security descriptor of a file or folder, and from the Access property you can read the list of access control entries (ACEs) in the DACL.7

# Inventory the ACLs of folders two levels deep from the share root
# The "trunk" of a permission design is concentrated in the shallow levels, so check there first
$aclErrors = @()   # -ErrorVariable with + appends, so reinitialize each run to avoid carrying over the previous run
$targets = @(Get-Item -LiteralPath $root `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors) +
           @(Get-ChildItem -LiteralPath $root -Directory -Recurse -Depth 1 `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors)

$aclReport = foreach ($t in $targets) {
    # Record retrieval failures (unreadable, deleted during the scan, and so on) so they can be disclosed later
    $acl = Get-Acl -LiteralPath $t.FullName -ErrorAction SilentlyContinue -ErrorVariable +aclErrors
    if (-not $acl) { continue }
    if (@($acl.Access).Count -eq 0) {
        # Keep a row for folders with an empty DACL (zero explicit entries) too, so broken-inheritance information does not vanish from the ledger
        [PSCustomObject]@{
            Path = $t.FullName; Identity = '(no entries)'; Rights = $null; Type = $null
            IsInherited = $null; Inheritance = $null
            InheritanceBroken = $acl.AreAccessRulesProtected
        }
        continue
    }
    foreach ($ace in $acl.Access) {
        [PSCustomObject]@{
            Path        = $t.FullName
            Identity    = $ace.IdentityReference    # Who (user/group)
            Rights      = $ace.FileSystemRights     # What they can do
            Type        = $ace.AccessControlType    # Allow / Deny
            IsInherited = $ace.IsInherited          # Inherited from the parent, or granted directly
            # Distinguish "this folder only" from "also applies to files/folders beneath it" for the same rights
            Inheritance = "$($ace.InheritanceFlags)/$($ace.PropagationFlags)"
            InheritanceBroken = $acl.AreAccessRulesProtected  # Is inheritance broken on this folder
        }
    }
}
$aclReport | Export-Csv -Path .\acl-report.csv -NoTypeInformation -Encoding utf8BOM   # Use UTF8 on 5.1

# Always disclose targets whose ACL could not be retrieved as holes in the ledger (same discipline as the other surveys)
$aclErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\acl-uninspected.txt

Once you open this CSV, filter it from the following angles to hunt for anomalies.

Column to look at Sign of trouble Typical background
Identity An individual user name appears directly The accumulation of “just give this person access for now.” Rots on transfers and departures
Identity Displayed as a raw SID such as S-1-5-21-... Permissions left behind by a deleted account (the first candidate for cleanup)
IsInherited = False Directly granted ACEs scattered deep in the tree Ad hoc individual grants. Exceptions that fell outside the design
InheritanceBroken = True A folder where inheritance has been broken A past response to “I don’t want this folder visible.” A key focus of the inventory
Type = Deny A deny ACE exists Under otherwise equal conditions, deny takes precedence over allow. However, explicit ACEs are evaluated before inherited ACEs, so there are orderings where an explicit allow beats an inherited deny. Trace the ACE’s origin to judge the impact, and confirm it ultimately with effective access

Entries where IsInherited is False are “permissions someone added by hand on that folder,” and folders where AreAccessRulesProtected is True are “islands that changes to the parent’s permissions never reach.”78 Most permission trouble concentrates in these two, so start your ledger there. One limitation should be stated explicitly: this example is a shallow scan with -Depth 1, so folders that break inheritance deeper than that are not covered, and no error is raised either. If you want a full survey of broken inheritance, either remove the -Depth restriction and run the same script (it will take time) or re-run it with a suspect departmental folder as the root. Note also that Get-Acl can display the security descriptor in the SDDL string format,7 but for inventory purposes the table from the Access property is enough. There is no hurry to get into reading SDDL until you meet a phenomenon this table cannot explain.

One practical caution. Always specify the character encoding of your CSVs. Windows PowerShell 5.1’s Export-Csv outputs ASCII by default, which corrupts non-ASCII text.10 The default for PowerShell 7.x is utf8NoBOM (UTF-8 without BOM),11 which is displayed as mojibake in some environments when opened directly in Excel. For a file you are handing to a department, the safe bet is to settle on UTF-8 with BOM, using -Encoding UTF8BOM (or -Encoding UTF8 on 5.1).

5. How to Make Changes — Choosing Between icacls and Set-Acl, Backups, and -WhatIf

Only once the inventory and departmental confirmation are finished do you enter the change phase. There are two families of tools here.

Purpose Tool Reason
Investigation and reporting Get-Acl Results are objects, so filtering and CSV export are easy7
Pre-change backup icacls /save Saves ACLs to a file and restores them as-is with /restore13
Routine granting and removal of permissions icacls /grant, /remove A one-liner, with /t for recursive application and inheritance re-enabling (/inheritancelevel)13
Complex conditional changes Set-Acl Rules can be assembled in code. Supports -WhatIf12
Rebuilding broken ACLs icacls /reset Replaces with the default inherited ACL (high impact; a last resort)13

Whichever you use, the order is fixed. Backup → -WhatIf (or target confirmation) → apply → verify afterwards.

# 1. Save the ACLs to a file before making changes (/t for everything beneath, /c to continue on errors)
icacls "D:\share\sales" /save "C:\aclbackup\sales-acl.txt" /t /c
# Do not proceed to changes without a backup. Check icacls's success or failure with the exit code
if ($LASTEXITCODE -ne 0) {
    throw "ACL backup failed (icacls ExitCode=$LASTEXITCODE). Aborting the change operation"
}

# 2. When changing with Set-Acl: three steps — retrieve, edit the rules, apply
$path = 'D:\share\sales\estimate'
$acl  = Get-Acl -LiteralPath $path

# A rule granting Modify rights to the sales group (inherited by subfolders and files)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'CONTOSO\SalesTeam', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
# Use AddAccessRule to grant additional rights. SetAccessRule replaces existing Allow rules for the same
# user/group, which can silently wipe out finer-grained permissions that were already configured for that group
$acl.AddAccessRule($rule)

# 3. Confirm "where this will be applied" with -WhatIf first, then proceed to the real run
Set-Acl -LiteralPath $path -AclObject $acl -WhatIf
# If everything looks fine: Set-Acl -LiteralPath $path -AclObject $acl

Because Set-Acl works by “applying the security descriptor obtained from Get-Acl as a model,”12 getting the target wrong between retrieval and application results in wholesale replacement of an ACL you never intended to touch. Unglamorous habits — holding the retrieval path and the application path in the same variable, as above, and inserting a -WhatIf before applying — are what prevent accidents. Breaking inheritance (SetAccessRuleProtection) is possible within the same framework,12 but as we saw in Section 4, broken inheritance becomes a future management cost, so limit new instances to places where it is genuinely necessary.

When you grow this sequence of work into a scheduled script, the design patterns (SupportsShouldProcess, keeping an audit trail, registering with Task Scheduler) covered in “Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting” can be applied as-is.

6. Where to Run It — Scanning Over UNC and the Executing Account

Finally, the topic of “where and as whom you run it,” which affects results more than you might expect.

  • As a rule, run large scans on the server itself. A recursive enumeration over a UNC path (\\fs01\share) turns per-file metadata retrieval into a network round trip, so on the scale of hundreds of thousands of files the elapsed time changes by an order of magnitude. If you want to work from an admin workstation, the practical arrangement is to run the script on the server via PowerShell Remoting and collect only the resulting CSV. For how to do that, see “An Introduction to PowerShell Remoting (WinRM),” published alongside this article.
  • The results are a snapshot of the executing account’s permissions. Places you cannot access are missing from both the enumeration and Get-Acl. Run the survey with an administrator account, and include any locations still denied (denied-paths.txt from Section 2) in the report as “places that could not be surveyed.”
  • When running over UNC, watch out for credential and session traps too. Drive letters are per-logon-session, you cannot connect to the same server with multiple sets of credentials (error 1219), and other pitfalls specific to share access are collected in “Pitfalls of Network Drives and UNC Paths.”

7. Standard Practice in the Field (Decision Table)

Question Options Rule of thumb
Scope of the capacity survey Everything at once / Per first-level folder Get a rough idea at a shallow level first, then dig only into the bloated places12
Handling errors Stop / Ignore / Record and continue SilentlyContinue + ErrorVariable for “continue but record everything.” State the denied zones explicitly in the report3
Criterion for old files LastAccessTime / LastWriteTime Access times are disabled in many environments, so they are out. Agree with the department on modified time + a period45
Duplicate detection Hash every file / Narrow by size, then hash Hashing is I/O heavy. Compute only for groups of identical size6
Depth of the ACL survey Every folder / Shallow levels + places with broken inheritance Where inheritance is alive, the parent tells you everything. Focus on IsInherited=False and Protected=True78
Tool for changing permissions icacls / Set-Acl icacls for routine changes and backups. Set-Acl + -WhatIf for bulk processing with conditional branches1312
Carrying out deletions Delete immediately / Quarantine → observe → delete After departmental confirmation of the list, first move to a quarantine folder, and delete if nothing goes wrong for a set period

8. Summary

  • Tidying a file server starts with an inventory, not deletion. Produce four CSV artefacts — a first-level capacity ranking, a list of old files, duplicate candidates, and an ACL ledger — and use them as material for the conversation with departments.
  • In exchange for not stopping the scan with SilentlyContinue, always record the denied locations with ErrorVariable. A place you could not count is not “zero,” it is “unknown.”
  • Base the “old” judgement on LastWriteTime. NTFS last access times have updates disabled in many environments and cannot be trusted.
  • For duplicates, narrow by size and then use Get-FileHash (SHA256 by default). A match is a “candidate”; deciding what to keep is done with the business side.
  • Export Get-Acl’s Access property to CSV and focus on direct individual grants, entries showing only a SID, and broken inheritance (AreAccessRulesProtected). There is no need to go deep into SDDL.
  • Make changes in this order: back up with icacls /save → -WhatIf or target confirmation → apply → verify afterwards. Run large scans on the server (or via Remoting), with administrator rights.

KomuraSoft LLC helps build scripts for taking inventory of file server capacity and permissions, carries out the investigation that accompanies a review of permission design, and automates periodic reporting (right through to Task Scheduler operation). We also welcome enquiries at the stage of “we just want to put numbers on the current situation first.”

References

  1. Microsoft Learn, Get-ChildItem. On enumerating items with Get-ChildItem, recursion with -Recurse and depth limiting with -Depth, and the recommendation to specify targets with -LiteralPath when using -Recurse in order to avoid wildcard interpretation.  2 3

  2. Microsoft Learn, Measure-Object. On Measure-Object being able to compute the sum, maximum, minimum, and average of file sizes with options such as -Property Length -Sum, and the example of aggregating files in a directory in combination with Get-ChildItem.  2 3

  3. Microsoft Learn, about_CommonParameters. On -ErrorAction SilentlyContinue suppressing the error display and continuing execution, -ErrorVariable storing error records in a specified variable, and prefixing the variable name with + appending rather than overwriting.  2 3 4

  4. Microsoft Learn, fsutil behavior. On NTFS last access time updates being enabled or disabled through the disablelastaccess parameter of fsutil behavior and the NtfsDisableLastAccessUpdate registry value, and disabling updates being provided in order to speed up file and directory access.  2 3

  5. Microsoft Learn, [MS-FSA]: Appendix A: Product Behavior. On NTFS/ReFS last access time updates being disabled by default from Windows Vista onwards, becoming system-managed from Windows 10 v1803 onwards, and last access time updates always being disabled on server systems.  2 3

  6. Microsoft Learn, Get-FileHash. On Get-FileHash’s default algorithm being SHA256, two files with matching hash values being judged identical in content, and hash values not changing when the file name or extension changes.  2 3

  7. Microsoft Learn, Get-Acl. On Get-Acl retrieving the security descriptor of a file or resource, displaying the list of DACL access control entries (Access) by default, and also being able to retrieve it in SDDL form (the Sddl property).  2 3 4 5 6

  8. Microsoft Learn, ObjectSecurity.AreAccessRulesProtected Property. On the AreAccessRulesProtected property returning whether the security descriptor’s DACL is protected (does not inherit from the parent).  2 3

  9. Microsoft Learn, Get-SmbShareAccess. On Get-SmbShareAccess being the cmdlet that retrieves an SMB share’s ACL (the security principals granted access to the share, together with allow/deny and rights). 

  10. Microsoft Learn, about_Character_Encoding. On default encodings being inconsistent per cmdlet in Windows PowerShell (5.1), with Export-Csv creating files in ASCII, and the default being unified to utf8NoBOM from PowerShell 6 onwards.  2

  11. Microsoft Learn, Export-Csv. On Export-Csv creating a CSV file with each property of an object as a column, and the default value of -Encoding in PowerShell 7.x being UTF8NoBOM, with UTF8BOM and others available as explicit options.  2

  12. Microsoft Learn, Set-Acl. On Set-Acl changing a target’s ACL using the security descriptor passed via AclObject as a model, supporting -WhatIf/-Confirm, the procedure of creating a FileSystemAccessRule and adding it with SetAccessRule(), and the example of disabling inheritance with SetAccessRuleProtection() (with the option to preserve existing inherited rules).  2 3 4 5

  13. Microsoft Learn, icacls. On icacls being the command for displaying and modifying DACLs, saving ACLs to a file with /save and applying (restoring) them with /restore, and the main options including /t (recursive), /c (continue on error), /grant, /remove, /reset, and /inheritancelevel.  2 3 4 5

  14. Microsoft Learn, Maximum Path Length Limitation. On the Windows API path length limit MAX_PATH being 260 characters, and the limit being relaxed for many Win32 functions only when both the LongPathsEnabled registry value and the application’s longPathAware declaration are in place. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

How do I find the total size of each folder with PowerShell?
Enumerate files with Get-ChildItem using -Recurse, then total them with Measure-Object using -Property Length -Sum. Rather than aggregating everything in one go, the practical approach is to first aggregate per first-level folder directly under the share root, to get a rough sense of where the bulk sits. When you do, use -ErrorAction SilentlyContinue together with -ErrorVariable and always record the locations you could not count because of access denials. If you silently swallow the errors, the places left out of the total will look like they are zero bytes.
Is it acceptable to use LastAccessTime for deciding which files are old?
It is not recommended. On NTFS, last access time updates are disabled by default (or system-managed) in many environments for performance reasons, so a file can be read without its timestamp ever being updated. Use LastWriteTime as the basis for your inventory and present it to departments as the time elapsed since the content was last changed. Do not make archiving decisions mechanically; have the stakeholders review the list before you proceed.
What can I learn from Get-Acl? Do I need to read SDDL?
Get-Acl retrieves the security descriptor of a file or folder, and from the Access property you can read who (IdentityReference), what they can do (FileSystemRights), whether it is an allow or a deny (AccessControlType), and whether it is inherited or granted directly (IsInherited). Looking at AreAccessRulesProtected also tells you whether inheritance has been broken on that folder. There is also a string form called SDDL, but for inventory purposes exporting the Access property to CSV is easier to share with stakeholders, and there is no need to go deep into SDDL.
Should I change ACLs with PowerShell's Set-Acl or with icacls?
As a rule of thumb, use Get-Acl for investigation and reporting because it gives you objects, and make icacls your first choice for actually making changes. icacls can save ACLs to a file with /save and restore them as-is with /restore, which makes pre-change backups and rollback easy. If you use Set-Acl, it becomes a three-step process — retrieve with Get-Acl, edit the rules, then apply — and you confirm the targets with -WhatIf before running it. In either case, the principle is to complete the inventory and get confirmation from the relevant departments before moving on to changes.
Is it fine to run the file server survey over a UNC path?
For small volumes there is no problem, but a full scan on the scale of hundreds of thousands of files is dramatically slower over the network. Where possible, run it directly on the file server (or via PowerShell Remoting) and bring back only the resulting CSV. Also, scan results depend on the permissions of the account running them. Folders you cannot access are missing from the enumeration, so run as an administrator account and then record any locations still denied, including them in the report as places that could not be surveyed.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog