Comparing folder security using PowerShell

Recently, a colleague alerted me to the Compare-Object cmdlet. In a nutshell: it compares two sets of objects and reports the differences, i.e. which elements are present in only one of those sets. So when we recently needed to compare the permissions on two folders and their subfolders (to make sure the copy was done correctly), it seemed almost trivial to automate that check using Powershell, because we have:

  • Get-ChildItem -Directory -Recurse to get a list of subfolders
  • (Get-Acl ...).Access to get the access rules for a folder
  • Compare-Object to compare the access rules of both sets of folders

 So basically we could do:

Compare-Object ((Get-ChildItem -Path $path1 -Directory -Recurse) | Get-Acl)
((Get-ChildItem -Path $path1=2 -Directory -Recurse) | Get-Acl)

but that produces output that's a little hard to read.

So here's the souped-up version: Get-FolderAccessRules.ps1

[CmdletBinding()]
Param(
  # The main path
  [Parameter(Mandatory)]
  [string]$Path,
  # An optional path to compare to
  [Parameter(Mandatory = $false)]
  [string]$ComparePath,
  # If -IncludeInherited, inherited permissions are also shown or compared
  [switch]$IncludeInherited,
  # If -PassThru, the results are returned (otherwise shown using Out-GridView)
  [switch]$PassThru
)

function CollectAccessRules([string]$path, [switch]$includeInherited) {
  # Get the full name of the path
  $root = (Get-Item $path).FullName

  # Loop over all folders in the path
  Write-Verbose "Collecting folders in $root..."
  $folders = Get-ChildItem -Path $root -Directory -Recurse | Sort-Object -Property FullName
  Write-Verbose "$root contains $($folders.Count) folders"

  # Get the access rules for each folder
  foreach ($folder in $folders) {
    Write-Verbose $folder.FullName
    $rules = (Get-Acl -Path $folder.FullName).Access
    foreach ($rule in $rules) {
      # Include this rule if it is not inherited, or we display inherited rules anyway
      if (!$rule.IsInherited -or $includeInherited) {
        # Create a custom object for each folder and rule with the rule properties expanded
        [PSCustomObject]@{
          Path        = $folder.FullName.Substring($root.Length + 1) # Make this a relative path name
          RuleType    = $rule.AccessControlType
          Identity    = $rule.IdentityReference
          Access      = $rule.FileSystemRights
          IsInherited = $rule.IsInherited
          Inheritance = $rule.InheritanceFlags
          Propagation = $rule.PropagationFlags
        }
      }
    }
  }
}

# Get the security rules for all folders in the main folder
$rules_1 = CollectAccessRules $Path -IncludeInherited:$IncludeInherited

# If no -ComparePath specified, just show the rules (or pass them through)
if (!$ComparePath) {
  if ($PassThru) {
    $rules_1
  } else {
    $rules_1 | Out-GridView -Title "Security access rules for '$Path'"
  }
} else {
  $rules_2 = CollectAccessRules $ComparePath -IncludeInherited:$IncludeInherited

  # Compare and display the differences
  $diff = Compare-Object $rules_1 $rules_2 -Property Path, Identity, RuleType, Access, Propagation, IsInherited, Inheritance
  
  if ($PassThru) {
    $diff
  } else {
    $diff | Out-GridView -Title "Security access rule differences '$Path' <=> '$ComparePath'"
  }
}

Usage:

Get-FolderAccessRules.ps1
[-Path] <string>
[[-ComparePath] <string>]
[-IncludeInherited]
[-PassThru]
[<CommonParameters>]

The -Path argument is mandatory. If there is no -ComparePath, the cmdlet will get all access rules on all folders and subfolders in the supplied path and display them.

If both -Path and -ComparePath are specified, access rules on both folders and subfolders are collected and the differences displayed.

To "display" means "pass to Out-GridView", unless -PassThru is specified. Using -PassThru, the access rules for each folder are returned.

By default, only non-inherited access rules are collected, i.e. the permissions that were explicitly set on a folder. To include inherited rules as well, specify the -IncludeInherited flag. Normally this produces way too much output, but it can be useful to double-check permisions.

The heavy lifting is done by the CollectAccessRules function. It first collects and sorts all subfolders of a folder, and then collects all access rules for each of those subfolders. To make the result meaningful, we need each access rule as a separate result, combined with the folder name. And in order to be able to compare them, each folder name must be made relative to it's "root" folder. The rest of the cmdlet is basically determining if we're in collect mode or compare mode (i.e. if -ComparePath if specified), collecting access rules on one or both folders and either returning the raw results or passing them to Out-GridView. Which makes our one-liner almost 70 lines of script, but hey.

Closing remark: if during comparison a folder exists in one path but not the other, its access rules will simply be displayed as differences.