# PAA ACL cleanup - Audit first, then set $DoRemoval = $true $Root = 'D:\' $AuditCsv = 'D:\acl-audit.csv' $FixLog = 'D:\acl-fix.log' # Mode flags $DoRemoval = $false # set to $true to apply removals after auditing $IncludeReparsePoints = $true # set to $true to include OneDrive cloud placeholders $ExcludeRootFromRemoval = $true # never modify explicit ACEs on the root of D: # Optional path whitelist - entire subtrees skipped $WhitelistPaths = @( 'D:\$RECYCLE.BIN\', 'D:\System Volume Information\', 'D:\Pierre-André\LocalApps\Vivaldi\Application' # add more with trailing backslash, for example: 'D:\KeepExplicit\' (and don't forget commas :) ) ) # ---------- Add this helper and one line right after $WhitelistPaths is defined --------------- function Normalize-Dir([string]$p) { ([System.IO.Path]::GetFullPath($p)).TrimEnd('\') + '\' } $WhitelistPaths = $WhitelistPaths | ForEach-Object { Normalize-Dir $_ } # ---------- Done # Identity whitelist - names or SIDs. Resolved to SIDs for robust matching. $WhitelistAccounts = @( 'BUILTIN\Administrateurs', 'AUTORITE NT\Système', 'AUTORITE NT\Utilisateurs authentifiés', 'NT AUTHORITY\SYSTEM', 'NT AUTHORITY\Authenticated Users', 'CREATOR OWNER' ) function Resolve-ToSidString { param([string]$Id) try { if ($Id -match '^S-\d+-') { return ([System.Security.Principal.SecurityIdentifier]$Id).Value } else { $nt = New-Object System.Security.Principal.NTAccount($Id) return $nt.Translate([System.Security.Principal.SecurityIdentifier]).Value } } catch { return $null } } $WhitelistSidSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($w in $WhitelistAccounts) { $sid = Resolve-ToSidString $w if ($sid) { $null = $WhitelistSidSet.Add($sid) } } function Test-IsUnder { param($Path, $Prefix) return $Path.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase) } # Normalize a path without trailing backslash for root comparisons function Normalize-Path([string]$p) { return [System.IO.Path]::GetFullPath($p).TrimEnd('\') } $rootNorm = Normalize-Path $Root # Collect items. We do not add the root object to $items to avoid accidental edits. $gciParams = @{ LiteralPath = $Root; Recurse = $true; Force = $true; ErrorAction = 'SilentlyContinue' } $items = if ($IncludeReparsePoints) { Get-ChildItem @gciParams } else { Get-ChildItem @gciParams -Attributes !ReparsePoint } $report = New-Object System.Collections.Generic.List[object] $index = 0 foreach ($item in $items) { $index++ Write-Progress -Activity 'Auditing ACLs' -Status $item.FullName -PercentComplete (($index / $items.Count) * 100) # Skip whitelisted paths $skip = $false foreach ($p in $WhitelistPaths) { if (Test-IsUnder $item.FullName $p) { $skip = $true; break } } if ($skip) { continue } # True only for the root folder itself, which we excluded from enumeration anyway if $items came from Get-ChildItem $isRoot = (Normalize-Path $item.FullName) -eq $rootNorm try { $acl = Get-Acl -LiteralPath $item.FullName # Ensure inheritance is enabled on each node if ($acl.AreAccessRulesProtected) { $acl.SetAccessRuleProtection($false, $true) Set-Acl -LiteralPath $item.FullName -AclObject $acl $acl = Get-Acl -LiteralPath $item.FullName } $explicitRules = $acl.Access | Where-Object { -not $_.IsInherited } foreach ($r in $explicitRules) { # Translate to SID for reliable whitelist checks $ruleSid = $null try { $ruleSid = $r.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { $ruleSid = $r.IdentityReference.Value } # Hide whitelisted identities from audit to keep CSV focused if ($WhitelistSidSet.Contains($ruleSid)) { continue } $report.Add([pscustomobject]@{ Path = $item.FullName Identity = $r.IdentityReference.Value Rights = $r.FileSystemRights Type = $r.AccessControlType InheritanceFlags = $r.InheritanceFlags PropagationFlags = $r.PropagationFlags }) } # ---------- Find the line that begins: if ($DoRemoval -and $explicitRules.Count -gt 0) { ---------- BLOC START HERE # if ($DoRemoval -and $explicitRules.Count -gt 0) { # if ($ExcludeRootFromRemoval -and $isRoot) { continue } # $changed = $false # foreach ($r in $explicitRules) { # $ruleSid = $null # try { $ruleSid = $r.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value } # catch { $ruleSid = $r.IdentityReference.Value } # if ($WhitelistSidSet.Contains($ruleSid)) { continue } # $null = $acl.RemoveAccessRuleSpecific($r) # $changed = $true # } # if ($changed) { # Set-Acl -LiteralPath $item.FullName -AclObject $acl # Add-Content -LiteralPath $FixLog -Value ('Removed explicit ACEs from: ' + $item.FullName) # } # } # } catch { # Add-Content -LiteralPath $FixLog -Value ('Error on: ' + $item.FullName + ' - ' + $_.Exception.Message) # continue # } # } # ---------- Find the line ---------- BLOC ENDS HERE # ---------- replace that whole block with the one below ---------- NEW BLOC START HERE if ($DoRemoval -and $explicitRules.Count -gt 0) { if ($ExcludeRootFromRemoval -and $isRoot) { continue } $changed = $false # Collect unique, non-whitelisted SIDs present in explicit rules $sidStrings = @() foreach ($r in $explicitRules) { $sidStr = $null try { $sidStr = $r.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { $sidStr = $r.IdentityReference.Value } if ($sidStr -and -not $WhitelistSidSet.Contains($sidStr)) { $sidStrings += $sidStr } } $sidStrings = $sidStrings | Select-Object -Unique # Purge all explicit ACEs for each target SID in one shot foreach ($sidStr in $sidStrings) { try { $sid = New-Object System.Security.Principal.SecurityIdentifier($sidStr) $acl.PurgeAccessRules($sid) $changed = $true } catch { # Last-resort fallback if the SID ctor fails for any reason foreach ($r in $explicitRules) { try { $null = $acl.RemoveAccessRuleSpecific($r); $changed = $true } catch {} } } } if ($changed) { Set-Acl -LiteralPath $item.FullName -AclObject $acl Add-Content -LiteralPath $FixLog -Value ('Purged explicit ACEs from: ' + $item.FullName) } } # ---------- replace that whole block with the one below ---------- NEW BLOC ENDS HERE $report | Sort-Object Path, Identity | Export-Csv -LiteralPath $AuditCsv -NoTypeInformation -Encoding UTF8 Write-Host '' Write-Host 'Audit written to ' $AuditCsv if ($DoRemoval) { Write-Host 'Changes logged to ' $FixLog } else { Write-Host 'Removal is disabled. Set $DoRemoval = $true to apply the cleanup.' }