Export-Permission.ps1
<#PSScriptInfo
.VERSION 0.0.91 .GUID fd2d03cf-4d29-4843-bb1c-0fba86b0220a .AUTHOR Jeremy La Camera .COMPANYNAME Jeremy La Camera .COPYRIGHT (c) Jeremy La Camera. All rights reserved. .TAGS .LICENSEURI https://github.com/IMJLA/ExportPermission/blob/main/LICENSE .PROJECTURI https://github.com/IMJLA/ExportPermission .ICONURI .EXTERNALMODULEDEPENDENCIES Adsi,SimplePrtg,PsNtfs,PsLogMessage,PsRunspace,PsDfs,PsBootstrapCss,Permission .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> #Requires -Module Adsi #Requires -Module SimplePrtg #Requires -Module PsNtfs #Requires -Module PsLogMessage #Requires -Module PsRunspace #Requires -Module PsDfs #Requires -Module PsBootstrapCss #Requires -Module Permission <# .SYNOPSIS Create CSV, HTML, and XML reports of permissions .DESCRIPTION Gets all permissions for the target folder Gets non-inherited permissions for subfolders (if specified) Exports the permissions to CSV Uses ADSI to get information about the accounts and groups listed in the permissions Exports information about the accounts and groups to CSV Uses ADSI to recursively retrieve the members of nested groups Creates an HTML report showing the resultant access of individual accounts Exports information about all accounts with NTFS access to CSV Creates an HTML report of all accounts with NTFS access Outputs an XML-formatted list of common misconfigurations for use win Paessler PRTG Network Monitor as a custom XML sensor .INPUTS None. Pipeline input is not accepted. .OUTPUTS [System.String] XML PRTG sensor output .NOTES TODO: Bug - Logic Flaw for Owner. Currently we search folders for non-inherited access rules, then we manually add a FullControl access rule for the Owner. This misses folders with only inherited access rules but a different owner. TODO: Bug - Doesn't work for AD users' default group/primary group (which is typically Domain Users). The user's default group is not listed in their memberOf attribute so I need to fix the LDAP search filter to include the primary group attribute. TODO: Bug - For a fake group created by New-FakeDirectoryEntry in the Adsi module, in the report its name will end up as an NT Account (CONTOSO\User123). If it is a fake user, its name will correctly appear without the domain prefix (User123) TODO: Feature - List any excluded accounts at the end TODO: Feature - Remove all usage of Add-Member to improve performance (create new pscustomobjects instead, nest original object inside) TODO: Feature - Parameter to specify properties to include in report TODO: Feature - This script does NOT account for individual file permissions. Only folder permissions are considered. TODO: Feature - This script does NOT account for file share permissions. Only NTFS permissions are considered. TODO: Feature - Support ACLs from Registry or AD objects TODO: Feature - psake task to update Release Notes in the script metadata to the github commit message .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test Generate reports on the NTFS permissions for the folder C:\Test and all subfolders .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -AccountsToSkip 'BUILTIN\\Administrator' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Exclude the built-in Administrator account from the HTML report The AccountsToSkip parameter uses RegEx, so the \ in BUILTIN\Administrator needed to be escaped. The RegEx escape character is \ so that is why the regular expression needed for the parameter is 'BUILTIN\\Administrator' .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -ExcludeEmptyGroups Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Exclude empty groups from the HTML report (leaving accounts only) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -DomainToIgnore 'CONTOSO' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Remove the CONTOSO domain prefix from associated accounts and groups .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LogDir C:\Logs Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Redirect logs and output files to C:\Logs instead of the default location in AppData .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LevelsOfSubfolders 0 Generate reports on the NTFS permissions for the folder C:\Test only (no subfolders) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -LevelsOfSubfolders 2 Generate reports on the NTFS permissions for the folder C:\Test Only include subfolders to a maximum of 2 levels deep (C:\Test\Level1\Level2) .EXAMPLE .\Export-Permission.ps1 -TargetPath C:\Test -Title 'New Custom Report Title' Generate reports on the NTFS permissions for the folder C:\Test and all subfolders Change the title of the HTML report to 'New Custom Report Title' #> param ( # Path to the folder whose permissions to report (only tested with local paths, UNC may work, unknown) [string]$TargetPath = 'C:\Test', #[string]$TargetPath = '\\ad.contoso.com\coh\Test2\FolderWithoutTarget\FolderWithTarget\', # Regular expressions that will identify Users or Groups you do not want included in the Html report [string[]]$AccountsToSkip<# = @( 'BUILTIN\\Administrators', 'BUILTIN\\Administrator', 'CREATOR OWNER', 'NT AUTHORITY\\SYSTEM' )#>, # Exclude empty groups from the HTML report [switch]$ExcludeEmptyGroups, # Domains to ignore (they will be removed from the username) # Intended when a user has matching SamAccountNames in multiple domains but you only want them to appear once on the report. [string[]]$DomainToIgnore, # = @('CONTOSO1\\','CONTOSO2\\'), # Path to save the logs and reports generated by this script [string]$LogDir = "$env:AppData\Export-Permission\Logs", # Path containing the required modules for this script # Each module must match proper PowerShell module folder structure (module folder name matches the name of the .psm1 file) [string]$ModulesDir = '$PSScriptRoot\Modules', # Get group members [switch]$NoGroupMembers, <# How many levels of subfolder to enumerate Set to 0 to ignore all subfolders Set to -1 (default) to recurse infinitely Set to any whole number to enumerate that many levels #> [int]$LevelsOfSubfolders = -1, # Title at the top of the HTML report [string]$Title = "Folder Permissions Report", <# Valid group names that are allowed to appear in ACEs Specify as a ScriptBlock meant for the FilterScript parameter of Where-Object In the scriptblock, use string comparisons on the Name property e.g. {$_.Name -like 'CONTOSO\Group1*' -or $_.Name -eq 'CONTOSO\Group23'} The naming format that will be used for the groups is CONTOSO\Group1 where CONTOSO is the NetBIOS name of the domain, and Group1 is the samAccountName of the group By default, this is a scriptblock that always evaluates to $true so it doesn't evaluate any naming convention compliance #> [scriptblock]$GroupNamingConvention = { $true }, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgProbe, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorProtocol, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [int]$PrtgSensorPort, # If all four of the PRTG parameters are specified, # the results will be XML-formatted and pushed to the specified PRTG probe for a push sensor [string]$PrtgSensorToken ) #----------------[ Initialization ]---------------- # $PSScriptRoot is usually null inside the param block so I can't use the double-quotes up there to expand it # doing it this way allows comment-based help to accurately reflect the default values of these parameters if ($ModulesDir -eq '$PSScriptRoot\Modules') { $ModulesDir = "$PSScriptRoot\Modules" } #----------------[ Functions ]------------------ # Load all modules contained in the specified directory Get-ChildItem -Directory -LiteralPath $ModulesDir | ForEach-Object { Remove-Module $_.Name -Force -ErrorAction SilentlyContinue *>$null Import-Module $_.FullName -ErrorAction Stop #*>$null } #----------------[ Logging ]---------------- $LogDir = New-DatedSubfolder -Root $LogDir Start-Transcript "$LogDir\Transcript.log" #----------------[ Declarations ]---------------- $DirectoryEntryCache = [hashtable]::Synchronized(@{}) $IdentityReferenceCache = [hashtable]::Synchronized(@{}) $AdsiServerCache = [hashtable]::Synchronized(@{}) $Permissions = $null $FolderTargets = $null $SecurityPrincipals = $null $AccountPermissions = $null $DedupedUserPermissions = $null $FolderPermissions = $null #----------------[ Main Execution ]--------------- Write-Verbose "$(Get-Date -Format s)`t$(hostname)`tExport-Permission`tTarget Folder: '$TargetPath'" $FolderTargets = Get-FolderTarget -FolderPath $TargetPath $ReportDescription = Get-ReportDescription -LevelsOfSubfolders $LevelsOfSubfolders $FolderTableHeader = Get-FolderTableHeader -LevelsOfSubfolders $LevelsOfSubfolders $Permissions = Get-FolderAccessList -FolderTargets $FolderTargets -LevelsOfSubfolders $LevelsOfSubfolders # If $TargetPath was on a local disk such as C:\ # The Get-FolderTarget cmdlet has replaced that local disk path with the corresponding UNC path \\$(hostname)\C$ # Unfortunately if it is the root of that local disk, Get-Item is unable to retrieve a DirectoryInfo object for the root of the share # (error: "Could not find item") # As a workaround here we will instead get the folder ACL for the original $TargetPath # But I don't think this solves it since it won't work for actual remote paths at the root of the share: \\server\share if ($null -eq $Permissions) { $Permissions = Get-FolderAccessList -FolderTargets $TargetPath -LevelsOfSubfolders $LevelsOfSubfolders } # Save a CSV of the raw NTFS ACEs, showing non-inherited ACEs only except for the root folder $TargetPath $NtfsAccessControlEntriesCsv = "$LogDir\NtfsAccessControlEntries.csv" $Permissions | Select-Object -Property @{ Label = 'Path' Expression = { $_.SourceAccessList.Path } }, IdentityReference, AccessControlType, FileSystemRights, IsInherited, PropagationFlags, InheritanceFlags | Export-Csv -NoTypeInformation -LiteralPath $NtfsAccessControlEntriesCsv Write-Information $NtfsAccessControlEntriesCsv # Resolve the Identity References directly from the NTFS ACEs to their associated SIDs/Names $ResolveAce = @{ Command = 'Resolve-Ace' InputObject = $Permissions InputParameter = 'InputObject' ObjectStringProperty = 'IdentityReference' AddParam = @{ KnownServers = $AdsiServerCache } } $Identities = Split-Thread @ResolveAce # Save a CSV report of the resolved identity references $IdentityReferencesCsv = "$LogDir\NtfsIdentityReferences.csv" $Identities | Select-Object -Property @{ Label = 'Path' Expression = { $_.SourceAccessList.Path } }, * | Export-Csv -NoTypeInformation -LiteralPath $IdentityReferencesCsv Write-Information $IdentityReferencesCsv $GroupedIdentities = $Identities | Group-Object -Property IdentityReferenceResolved # Use ADSI to collect more information about each IdentityReference (e.g. CONTOSO\user1) in NTFS Access Control Entries $ExpandIdentityReference = @{ Command = 'Expand-IdentityReference' InputObject = $GroupedIdentities InputParameter = 'AccessControlEntry' AddParam = @{ DirectoryEntryCache = $DirectoryEntryCache IdentityReferenceCache = $IdentityReferenceCache } ObjectStringProperty = 'Name' } if ($NoGroupMembers) { $ExpandIdentityReference['AddSwitch'] = 'NoGroupMembers' } $SecurityPrincipals = Split-Thread @ExpandIdentityReference # Format Security Principals (distinguish group members from users directly listed in the NTFS DACLs) # Filter out groups (their members have already been retrieved) $FormatSecurityPrincipal = @{ Command = 'Format-SecurityPrincipal' InputObject = $SecurityPrincipals InputParameter = 'SecurityPrincipal' Timeout = 1200 ObjectStringProperty = 'Name' } $AccountPermissions = Split-Thread @FormatSecurityPrincipal $ExpandedAccountPermissions = Expand-AccountPermission -AccountPermission $AccountPermissions # Save a CSV report of the expanded account permissions #TODO: Expand DirectoryEntry objects in the DirectoryEntry and Members properties $ExpandedAccountPermissionsCsv = "$LogDir\ResultantAccountPermissions.csv" $ExpandedAccountPermissions | Select-Object -Property @{ Label = 'SourceAclPath' Expression = { $_.ACESourceAccessList.Path } }, * | Export-Csv -NoTypeInformation -LiteralPath $ExpandedAccountPermissionsCsv Write-Information $ExpandedAccountPermissionsCsv $Accounts = $AccountPermissions | Group-Object -Property User | Sort-Object -Property Name # Ensure accounts only appear once on the report if they exist in multiple domains $DedupedUserPermissions = $Accounts | Remove-DuplicatesAcrossIgnoredDomains -DomainToIgnore $DomainToIgnore # Group the user permissions back into folder permissions for the report $FolderPermissions = Format-FolderPermission -UserPermission $DedupedUserPermissions | Group-Object -Property Folder | Sort-Object -Property Name $HtmlTableOfFolders = Select-FolderTableProperty -InputObject $FolderPermissions | ConvertTo-Html -Fragment | New-BootstrapTable $GetFolderPermissionsBlock = @{ FolderPermissions = $FolderPermissions AccountsToSkip = $AccountsToSkip ExcludeEmptyGroups = $ExcludeEmptyGroups DomainToIgnore = $DomainToIgnore } $HtmlFolderPermissions = Get-FolderPermissionsBlock @GetFolderPermissionsBlock ##Commented the two lines below because actually keeping semicolons means it copy/pastes better into Excel ### Convert-ToHtml will not expand in-line HTML, so we had to use semicolons as placeholders and will now replace them with line breaks. ##$HtmlFolderPermissions = $HtmlFolderPermissions -replace ' ; ','<br>' $ReportDescription = "$(New-BootstrapAlert -Class Dark -Text $TargetPath) $ReportDescription" $FolderList = Get-HtmlFolderList -FolderTableHeader $FolderTableHeader -HtmlTableOfFolders $HtmlTableOfFolders [string]$Body = Get-HtmlBody -FolderList $FolderList -HtmlFolderPermissions $HtmlFolderPermissions $ReportParameters = @{ Title = $Title Description = $ReportDescription Body = $Body } $Report = New-BootstrapReport @ReportParameters # Save the Html report $ReportFile = "$LogDir\FolderPermissionsReport.html" $Report | Set-Content -LiteralPath $ReportFile # Output the name of the report file to the Information stream Write-Information $ReportFile # Report common issues with NTFS permissions (formatted as XML for PRTG) # TODO: Users with ownership $NtfsIssueParams = @{ FolderPermissions = $FolderPermissions UserPermissions = $Accounts GroupNamingConvention = $GroupNamingConvention } $NtfsIssues = New-NtfsAclIssueReport @NtfsIssueParams # Format the information as a custom XML sensor for Paessler PRTG Network Monitor $XMLOutput = Get-PrtgXmlSensorOutput -NtfsIssues $NtfsIssues # Save the result of the custom XML sensor for Paessler PRTG Network Monitor $XmlFile = "$LogDir\PrtgSensorResult.xml" $XMLOutput | Set-Content -LiteralPath $XmlFile # Output the name of the report file to the Information stream Write-Information $XmlFile # Send the XML to a PRTG Custom XML Push sensor for tracking $PrtgSensorParams = @{ XmlOutput = $XMLOutput PrtgProbe = $PrtgProbe PrtgSensorProtocol = $PrtgSensorProtocol PrtgSensorPort = $PrtgSensorPort PrtgSensorToken = $PrtgSensorToken } Send-PrtgXmlSensorOutput @PrtgSensorParams Stop-Transcript # Output the XML so the script can be directly used as a PRTG sensor Invoke-Item $ReportFile # Output the XML so the script can be directly used as a PRTG sensor return $XMLOutput |