Public/Get-DirectUserACEs.ps1
|
function Get-DirectUserACEs { <# .SYNOPSIS Finds ACL entries assigned directly to user accounts rather than groups. .DESCRIPTION Scans NTFS permissions on the specified paths and identifies access control entries that reference individual user accounts instead of security groups. Direct user ACEs are a common compliance finding because they bypass group-based access control and make permission management error-prone. Departed employees with direct ACEs are a frequent audit failure. .PARAMETER Path One or more file system paths to scan. Accepts pipeline input. .PARAMETER MaxDepth Maximum folder recursion depth. Defaults to 3 to prevent runaway scans. .PARAMETER ExcludeBuiltIn When set (default: true), excludes built-in accounts such as NT AUTHORITY\SYSTEM, BUILTIN\Administrators, NT AUTHORITY\LOCAL SERVICE, etc. These are expected to have direct ACEs. .EXAMPLE Get-DirectUserACEs -Path '\\server\share\Finance' .EXAMPLE '\\server\share\Finance', '\\server\share\HR' | Get-DirectUserACEs -MaxDepth 5 #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [string[]]$Path, [Parameter()] [ValidateRange(1, 20)] [int]$MaxDepth = 3, [Parameter()] [switch]$ExcludeBuiltIn = $true ) begin { # Built-in accounts that are expected to have direct ACEs $builtInPatterns = @( 'NT AUTHORITY\SYSTEM' 'NT AUTHORITY\LOCAL SERVICE' 'NT AUTHORITY\NETWORK SERVICE' 'BUILTIN\Administrators' 'BUILTIN\Users' 'BUILTIN\IIS_IUSRS' 'CREATOR OWNER' 'NT SERVICE\*' ) function Test-BuiltInIdentity { param([string]$Identity) foreach ($pattern in $builtInPatterns) { if ($Identity -like $pattern) { return $true } } return $false } function Get-IdentityType { <# .SYNOPSIS Determines whether an identity reference is a user or group account. #> param([string]$Identity) # Strip domain prefix for lookup $accountName = $Identity if ($Identity -match '^(.+)\\(.+)$') { $accountName = $Matches[2] } # Try AD group first (groups are the expected case) try { $null = Get-ADGroup -Identity $accountName -ErrorAction Stop return 'Group' } catch { # Not a group - continue } # Try AD user try { $null = Get-ADUser -Identity $accountName -ErrorAction Stop return 'User' } catch { # Not found in AD - could be local or orphaned SID } # Check if it looks like an unresolved SID (departed employee) if ($Identity -match '^S-1-5-21-') { return 'User' # Unresolved SIDs are almost always departed users } return 'Unknown' } function Get-FoldersToDepth { param( [string]$RootPath, [int]$Depth ) $folders = @([PSCustomObject]@{ FullName = $RootPath; Depth = 0 }) if ($Depth -ge 1) { $children = Get-ChildItem -Path $RootPath -Directory -Recurse -Depth ($Depth - 1) -ErrorAction SilentlyContinue foreach ($child in $children) { $relativePath = $child.FullName.Substring($RootPath.Length).TrimStart('\', '/') $currentDepth = ($relativePath -split '[\\/]').Count $folders += [PSCustomObject]@{ FullName = $child.FullName; Depth = $currentDepth } } } $folders } $results = [System.Collections.Generic.List[PSObject]]::new() } process { foreach ($scanPath in $Path) { if (-not (Test-Path -Path $scanPath)) { Write-Warning "Path not found: $scanPath" continue } Write-Verbose "Scanning for direct user ACEs: $scanPath (MaxDepth: $MaxDepth)" $folders = Get-FoldersToDepth -RootPath $scanPath -Depth $MaxDepth foreach ($folder in $folders) { try { $acl = Get-Acl -Path $folder.FullName -ErrorAction Stop } catch { Write-Warning "Cannot read ACL on $($folder.FullName): $_" continue } foreach ($ace in $acl.Access) { $identity = $ace.IdentityReference.Value # Skip built-in accounts if requested if ($ExcludeBuiltIn -and (Test-BuiltInIdentity -Identity $identity)) { continue } $objectType = Get-IdentityType -Identity $identity $finding = if ($objectType -eq 'User') { 'DIRECT USER ACE' } else { 'OK' } $result = [PSCustomObject]@{ Path = $folder.FullName Identity = $identity AccessType = $ace.AccessControlType.ToString() Rights = $ace.FileSystemRights.ToString() IsInherited = $ace.IsInherited ObjectType = $objectType Finding = $finding } $results.Add($result) } } } } end { $results } } |