Get ACL Information with PowerShell

I got a question in the “Ask Don and Jeff” forum on PowerShell.com that intrigued me. The issue was working with the results of the Get-ACL cmdlet. The resulting object includes a property called Access which is a collection of access rule objects. Assuming you are using this with the file system, these are System.Security.AccessControl.FileSystemAccessRule objects that look like this:

FileSystemRights : ReadAndExecute, Synchronize
AccessControlType : Allow
IdentityReference : BUILTIN\Users
IsInherited : False
InheritanceFlags : ContainerInherit, ObjectInherit
PropagationFlags : None

If I’m understanding the original problem, the poster wanted to identify folders that had a single non-system entry. That is, a folder where someone added a single entry. It doesn’t matter who. So this got me to thinking about a tool that would look at a folder ACL and report on how many access rules were found and then break that count down by system and user. I figured that any rule where the identity reference name included “Builtin”, “NT Authority”, “Everyone” or “Creator Owner” would be considered a system rule. Anything else would be considered a user rule.

In the console, I could run a command like this:

PS S:\> get-acl c:\work | select -expand access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

FileSystemRights  : FullControl
AccessControlType : Allow
IdentityReference : SERENITY\Jeff
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

Because I ran this interactively, I know what folder has this potential issue. So the next step is to turn this into a tool that will write ACL summary information to the pipeline. Here’s my function, and then I’ll explain a few things. The download version includes comment based help.

Function Get-ACLInfo {

[cmdletbinding()]

Param(
[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateScript({Test-Path $_})]
[Alias('PSPath','Fullname')]
[string[]]$Path="."
)

Begin {
    Write-Verbose "Starting $($myinvocation.mycommand)"
   
    #create a format file on the fly
    $xml=@"
<?xml version="
1.0" encoding="utf-8" ?>
<Configuration>
    <ViewDefinitions>
        <View>
            <Name>JDH.ACLInfo</Name>
            <ViewSelectedBy>
                <TypeName>JDH.ACLInfo</TypeName>
            </ViewSelectedBy>
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Width>50</Width>
                    </TableColumnHeader>
                    <TableColumnHeader/>
                    <TableColumnHeader>
                      <Width>8</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                      <Width>9</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>7</Width>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <PropertyName>Path</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Owner</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>TotalACL</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <Propertyname>SystemACL</Propertyname>
                            </TableColumnItem>
                             <TableColumnItem>
                                <Propertyname>UserACL</Propertyname>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                 </TableRowEntries>
            </TableControl>
        </View>
    </ViewDefinitions>
</Configuration>
"
@
    #create a temp file
    $tmpfile=[system.io.path]::GetTempFileName()
    #add the necessary file extension
    $tmpfile+=".ps1xml"
   
    #pipe the xml text to the temp file
    Write-Verbose "Creating $tmpfile"
    $xml | Out-File -FilePath $tmpfile
   
    <#
     update format data. I'm setting error action to SilentlyContinue
     because everytime you run the function it creates a new temp file
     but Update-FormatData tries to reload all the format files it
     knows about in the current session, which includes previous versions
     of this file which have already been deleted.
    #>

    Write-Verbose "Updating format data"
    Update-FormatData -AppendPath $tmpfile -ErrorAction SilentlyContinue
   
} #Begin

Process {
    Foreach ($folder in $path) {
        Write-Verbose "Getting ACL for $folder"
        #get the folder ACL
        $acl=Get-ACL -Path $path
       
        #a regex to get a file path
        [regex]$regex="\w:\\\S+"
       
        #get full path from ACL object
        $folderpath=$regex.match($acl.path).Value
       
        #get Access rules
        $access=$acl.Access
       
        #get builtin and system ACLS
        $sysACL=$access | where {$_.identityreference -match "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}
               
        #get non builtin and system ACLS
        $nonSysACL=$access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}
       
        #grab some properties and add them to a hash table.
        $hash=@{
            Path=$folderpath
            Owner=$acl.Owner
            TotalACL=$access.count
            SystemACL=($sysACL | measure-object).Count
            UserACL=($nonSysACL | measure-object).Count
            AccessRules=$access
        }
               
        #write a new object to the pipeline
        $obj=New-object -TypeName PSObject -Property $hash
        #add a type name for the format file
        $obj.PSObject.TypeNames.Insert(0,'JDH.ACLInfo')
        $obj
   
    } #foreach

} #Process

End {
    #delete the temp file if it still exists
    if (Test-Path $tmpfile) {
        Write-Verbose "Deleting $tmpfile"
        Remove-Item -Path $tmpFile
    }
    Write-Verbose "Ending $($myinvocation.mycommand)"
} #end

} #end function

When I run the function here’s what the result looks like:

PS S:\> get-aclinfo | select *

SystemACL   : 7
Owner       : SERENITY\Jeff
UserACL     : 0
AccessRules : {System.Security.AccessControl.FileSystemAccessRule, System.Security.AccessControl.Fi
              leSystemAccessRule, System.Security.AccessControl.FileSystemAccessRule, System.Securi
              ty.AccessControl.FileSystemAccessRule...}
Path        : C:\scripts\
TotalACL    : 7

The function takes a path parameter and passes that to Get-ACL.

$acl=Get-ACL -Path $path

I will be using some of the properties of this object in the custom object I’ll eventually write to the pipeline. One thing I want is the full path. Unfortunately, I need to parse that out of the path property. I decided to use a regular expression.

#a regex to get a file path
[regex]$regex="\w:\\\S+"
   
#get full path from ACL object
$folderpath=$regex.match($acl.path).Value

Next, I need to count the access rule entries and determine which are system and which are user.

#get Access rules
$access=$acl.Access

#get builtin and system ACLS
$sysACL=$access | where {$_.identityreference -match "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}
               
#get non builtin and system ACLS
$nonSysACL=$access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

I like creating new objects with hash tables, which will get even easier in PowerShell 3.0.

#grab some properties and add them to a hash table.
$hash=@{
    Path=$folderpath
    Owner=$acl.Owner
    TotalACL=$access.count
    SystemACL=($sysACL | measure-object).Count
    UserACL=($nonSysACL | measure-object).Count
    AccessRules=$access
}
       
#write a new object to the pipeline
$obj=New-object -TypeName PSObject -Property $hash

The object shows counts of the different ACL “types” and also includes a property with the full access rules should you want to look at them in more detail. But now for the “scoop of ice cream on the side” part of this function.

In PowerShell 2.0, hash tables are unordered meaning there’s no guarantee what order your properties will be display. Plus, you may want to have more control over how PowerShell formats the resulting objects. The way we handle this is by creating a format file and loading it into the shell with Update-FormatData. I’m not going to go into the mechanics of custom format files here. I know the topic is covered in the Windows PowerShell 2.0: TFM book, and the Month of Lunches books among others.

Now, if I had created a module out of this function I could have packaged it with a separate format ps1xml file. But I had a thought of trying to use a format file “on the fly”. In the Begin script block, I have the XML that would normally go into the format.ps1xml file. I create a temp file and add the xml to it.

#create a temp file
$tmpfile=[system.io.path]::GetTempFileName()
#add the necessary file extension
$tmpfile+=".ps1xml"

#pipe the xml text to the temp file
Write-Verbose "Creating $tmpfile"
$xml | Out-File -FilePath $tmpfile

The format file apparently needs to have .ps1xml file extension so I have to update the file name. Once I have this file, I call Update-FormatData to load it.

Update-FormatData -AppendPath $tmpfile -ErrorAction SilentlyContinue

Normally, I’m not a big fan of turning off errors, but in this case I need to. When you run Update-FormatData, PowerShell reloads all the format files that it knows in this session. The first time I run the function, the temp file is created and loaded. But in the End script block, I delete it.

if (Test-Path $tmpfile) {
      Write-Verbose "Deleting $tmpfile"
      Remove-Item -Path $tmpFile
   }

The next time I run the function, I recreate the format file. But Update-FormatData also looks for the previous temp files which have since been deleted, which normally raises an exception. I’m not saying this approach of “on the fly formatting” is perfect but so far it works for me in these situations. The formatting file takes my custom object and creates a table with all properties except the access rules.

My format file allows for plenty of space for the file path. But you can always tighten things up.

If I want to work with the underlying access rules, I still can.

Download Get-ACLInfo and let me know what you think.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati


About the Author

PowerShell.org Announcer

This is the official account for PowerShell.org and sponsor announcements.