Update-AclSid.ps1
|
<# .SYNOPSIS Replace a SID in NTFS file/directory ACLs without breaking the original ACE structure. .DESCRIPTION Update-AclSid walks the ACL (DACL and optionally SACL) of each target file or directory and replaces every Access Control Entry (ACE) whose SecurityIdentifier matches -SourceSid with a new ACE that uses -TargetSid instead. All other ACE properties are preserved byte-for-byte: - AceType (Allow / Deny / Callback / ...) - AccessMask (the exact rights bitmask) - AceFlags (inheritance, propagation, audit flags) - IsInherited status - Canonical ACE ordering (Explicit-Deny -> Explicit-Allow -> Inherited-Deny -> Inherited-Allow) The cmdlet uses the low-level RawSecurityDescriptor / RawAcl / CommonAce API so that ACEs are replaced in-place via the indexer (acl[i] = newAce). This avoids the reordering side-effect of the high-level FileSystemAccessRule + AddAccessRule/RemoveAccessRule API. Owner and Group are NOT touched unless -ReplaceOwner / -ReplaceGroup is specified. .PARAMETER Path Target file or directory path(s). Accepts pipeline input and wildcards. .PARAMETER SourceSid The SID to be replaced. Accepts S-1-5-... string or DOMAIN\user account name. .PARAMETER TargetSid The replacement SID. Accepts S-1-5-... string or DOMAIN\user account name. .PARAMETER Recurse Recursively process all subdirectories and files. Reparse points are skipped by default. .PARAMETER IncludeInherited Also replace inherited ACEs. By default only explicit ACEs are replaced. Note: inherited ACEs are normally regenerated from the parent during inheritance propagation, so replacing them on a child is only durable if the parent is also updated (use -Recurse) or inheritance is broken on the child. .PARAMETER IncludeAudit Also replace SIDs in the SACL (audit rules). Requires SeSecurityPrivilege (admin). .PARAMETER ReplaceOwner Replace the Owner field when it equals the source SID. .PARAMETER ReplaceGroup Replace the Group field when it equals the source SID. .PARAMETER FollowReparsePoints Follow reparse points (symlinks, junctions) during recursion. Default: skip. .PARAMETER StopOnError Stop on the first error instead of continuing to the next entry. .PARAMETER PassThru Output a ReplaceResult object for each processed entry. .EXAMPLE Update-AclSid -Path C:\Data -SourceSid S-1-5-21-100-200-300-1001 -TargetSid S-1-5-21-100-200-300-2002 -Recurse -WhatIf Dry-run: show what would be changed without writing. .EXAMPLE Get-ChildItem C:\Data -Recurse | Update-AclSid -SourceSid 'OLDDOMAIN\user1' -TargetSid 'NEWDOMAIN\user1' -PassThru Pipeline input, output results for each file. .EXAMPLE Update-AclSid -Path C:\Shared -SourceSid 'S-1-5-21-1-2-3-1001' -TargetSid 'S-1-5-21-1-2-3-2002' -Recurse -ReplaceOwner -ReplaceGroup -IncludeAudit Full replacement including owner, group, and audit rules. .NOTES Requires Windows + .NET FileSystemAcl classes. Run as Administrator for system directories or to read SACL. #> function Update-AclSid { [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByPath')] [OutputType([pscustomobject])] param( [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByPath')] [Alias('FullName', 'PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, [Parameter(Mandatory, Position = 1)] [Alias('From', 'OldSid')] [string]$SourceSid, [Parameter(Mandatory, Position = 2)] [Alias('To', 'NewSid')] [string]$TargetSid, [switch]$Recurse, [switch]$IncludeInherited, [switch]$IncludeAudit, [switch]$ReplaceOwner, [switch]$ReplaceGroup, [switch]$FollowReparsePoints, [switch]$StopOnError, [switch]$PassThru ) begin { # ---------- 加载 ACL 类型 ---------- $aclTypes = @( 'System.Security.AccessControl.RawSecurityDescriptor' 'System.Security.AccessControl.RawAcl' 'System.Security.AccessControl.CommonAce' 'System.Security.AccessControl.ObjectCommonAce' 'System.Security.AccessControl.AceFlags' 'System.Security.AccessControl.AceType' 'System.Security.AccessControl.ObjectAceFlags' 'System.Security.AccessControl.AccessControlSections' 'System.Security.AccessControl.DirectorySecurity' 'System.Security.AccessControl.FileSecurity' 'System.Security.Principal.SecurityIdentifier' 'System.Security.Principal.NTAccount' ) foreach ($t in $aclTypes) { if (-not ($t -as [type])) { try { Add-Type -AssemblyName 'System.IO.FileSystem.AccessControl' -ErrorAction Stop } catch {} break } } # ---------- 解析 SID ---------- function ConvertTo-SecurityIdentifier { param([string]$sid_text) if ($sid_text -match '^S-1-\d') { try { return [System.Security.Principal.SecurityIdentifier]::new($sid_text) } catch { throw "无法解析 SID 字符串 '$sid_text': $($_.Exception.Message)" } } try { $nt = [System.Security.Principal.NTAccount]::new($sid_text) return $nt.Translate([System.Security.Principal.SecurityIdentifier]) } catch { throw "无法将 '$sid_text' 解析为 SID 或账户名。请使用 S-1-5-... 格式或 DOMAIN\user 格式。" } } try { $srcId = ConvertTo-SecurityIdentifier $SourceSid } catch { throw $_ } try { $dstId = ConvertTo-SecurityIdentifier $TargetSid } catch { throw $_ } if ($srcId.Value -eq $dstId.Value) { throw "源 SID 与目标 SID 相同,无需替换。" } Write-Verbose "源 SID: $($srcId.Value) ($SourceSid)" Write-Verbose "目标 SID: $($dstId.Value) ($TargetSid)" # ---------- 核心:在 RawAcl 中原地替换 SID ---------- function Replace-SidsInRawAcl { param( [System.Security.AccessControl.RawAcl]$Acl, [System.Security.Principal.SecurityIdentifier]$SourceId, [System.Security.Principal.SecurityIdentifier]$TargetId, [bool]$IncludeInherited ) $changes = 0 for ($i = 0; $i -lt $Acl.Count; $i++) { $ace = $Acl[$i] # 跳过继承的 ACE(除非显式要求) $isInherited = ($ace.AceFlags -band [System.Security.AccessControl.AceFlags]::Inherited) -ne 0 if (-not $IncludeInherited -and $isInherited) { continue } if ($ace -is [System.Security.AccessControl.CommonAce]) { if ($ace.SecurityIdentifier.Value -eq $SourceId.Value) { $newAce = [System.Security.AccessControl.CommonAce]::new( $ace.AceFlags, $ace.AceType, $ace.AccessMask, $TargetId, $ace.IsCallback, $ace.GetOpaque() ) $Acl[$i] = $newAce $changes++ } } elseif ($ace -is [System.Security.AccessControl.ObjectCommonAce]) { if ($ace.SecurityIdentifier.Value -eq $SourceId.Value) { $newAce = [System.Security.AccessControl.ObjectCommonAce]::new( $ace.AceFlags, $ace.AceType, $ace.AccessMask, $TargetId, $ace.ObjectAceFlags, $ace.ObjectType, $ace.InheritedObjectAceType, $ace.IsCallback, $ace.GetOpaque() ) $Acl[$i] = $newAce $changes++ } } } return $changes } # ---------- 单路径处理 ---------- function Invoke-SingleReplace { param( [string]$ResolvedPath, [bool]$WhatIfMode ) $sw = [System.Diagnostics.Stopwatch]::StartNew() $result = [pscustomobject]@{ Path = $ResolvedPath EntryType = '' Success = $false Error = $null DaclAceReplaced = 0 SaclAceReplaced = 0 OwnerReplaced = $false GroupReplaced = $false Written = $false ElapsedMilliseconds = 0 } try { $isDir = [System.IO.Directory]::Exists($ResolvedPath) $isFile = [System.IO.File]::Exists($ResolvedPath) if (-not $isDir -and -not $isFile) { $result.Error = '路径不存在' return $result } $result.EntryType = if ($isDir) { 'Directory' } else { 'File' } # 读取安全描述符 $sections = [System.Security.AccessControl.AccessControlSections]::Owner ` -bor [System.Security.AccessControl.AccessControlSections]::Group ` -bor [System.Security.AccessControl.AccessControlSections]::Access if ($IncludeAudit) { $sections = $sections -bor [System.Security.AccessControl.AccessControlSections]::Audit } if ($isDir) { $sd = [System.Security.AccessControl.DirectorySecurity]::new($ResolvedPath, $sections) } else { $sd = [System.Security.AccessControl.FileSecurity]::new($ResolvedPath, $sections) } $sdBytes = $sd.GetSecurityDescriptorBinaryForm() $rawSd = [System.Security.AccessControl.RawSecurityDescriptor]::new($sdBytes, 0) # DACL if ($null -ne $rawSd.DiscretionaryAcl) { $result.DaclAceReplaced = Replace-SidsInRawAcl -Acl $rawSd.DiscretionaryAcl -SourceId $srcId -TargetId $dstId -IncludeInherited $IncludeInherited } # SACL if ($IncludeAudit -and $null -ne $rawSd.SystemAcl) { $result.SaclAceReplaced = Replace-SidsInRawAcl -Acl $rawSd.SystemAcl -SourceId $srcId -TargetId $dstId -IncludeInherited $IncludeInherited } # Owner if ($ReplaceOwner -and $null -ne $rawSd.Owner -and $rawSd.Owner.Value -eq $srcId.Value) { $rawSd.Owner = $dstId $result.OwnerReplaced = $true } # Group if ($ReplaceGroup -and $null -ne $rawSd.Group -and $rawSd.Group.Value -eq $srcId.Value) { $rawSd.Group = $dstId $result.GroupReplaced = $true } $hasChanges = $result.DaclAceReplaced -gt 0 -or $result.SaclAceReplaced -gt 0 -or $result.OwnerReplaced -or $result.GroupReplaced if (-not $hasChanges) { $result.Success = $true $result.Written = $false return $result } if ($WhatIfMode) { $result.Success = $true $result.Written = $false return $result } # 写回 $newSdBytes = New-Object byte[] $rawSd.BinaryLength $rawSd.GetBinaryForm($newSdBytes, 0) if ($isDir) { $newSd = [System.Security.AccessControl.DirectorySecurity]::new() $newSd.SetSecurityDescriptorBinaryForm($newSdBytes) Set-Acl -Path $ResolvedPath -AclObject $newSd } else { $newSd = [System.Security.AccessControl.FileSecurity]::new() $newSd.SetSecurityDescriptorBinaryForm($newSdBytes) Set-Acl -Path $ResolvedPath -AclObject $newSd } $result.Success = $true $result.Written = $true } catch [System.Security.AccessControl.PrivilegeNotHeldException] { $result.Error = "特权不足(读取 SACL 需要 SeSecurityPrivilege): $($_.Exception.Message)" } catch [System.UnauthorizedAccessException] { $result.Error = "访问被拒绝(需要管理员权限): $($_.Exception.Message)" } catch { $result.Error = $_.Exception.Message } finally { $sw.Stop() $result.ElapsedMilliseconds = $sw.ElapsedMilliseconds } return $result } # ---------- 递归遍历 ---------- function Invoke-RecursiveReplace { param([string]$RootPath, [bool]$WhatIfMode) # 根路径 $r = Invoke-SingleReplace -ResolvedPath $RootPath -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0 -or $r.SaclAceReplaced -gt 0 -or $r.OwnerReplaced -or $r.GroupReplaced) { Write-Verbose $r.ToString() } if (-not $Recurse) { return } if (-not [System.IO.Directory]::Exists($RootPath)) { return } $stack = [System.Collections.Generic.Stack[string]]::new() $stack.Push($RootPath) while ($stack.Count -gt 0) { $current = $stack.Pop() # 子目录 $dirs = @() try { $dirs = [System.IO.Directory]::EnumerateDirectories($current, '*', [System.IO.SearchOption]::TopDirectoryOnly) } catch { continue } foreach ($dir in $dirs) { if (-not $FollowReparsePoints) { try { $attrs = [System.IO.File]::GetAttributes($dir) if (($attrs -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { continue } } catch {} } $r = Invoke-SingleReplace -ResolvedPath $dir -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0 -or $r.SaclAceReplaced -gt 0 -or $r.OwnerReplaced -or $r.GroupReplaced) { Write-Verbose $r.ToString() } $stack.Push($dir) } # 文件 $files = @() try { $files = [System.IO.Directory]::EnumerateFiles($current, '*', [System.IO.SearchOption]::TopDirectoryOnly) } catch { continue } foreach ($file in $files) { $r = Invoke-SingleReplace -ResolvedPath $file -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0 -or $r.SaclAceReplaced -gt 0 -or $r.OwnerReplaced -or $r.GroupReplaced) { Write-Verbose $r.ToString() } } } } } process { foreach ($p in $Path) { # 展开通配符 $resolved = @() try { $resolved = Resolve-Path -Path $p -ErrorAction Stop | Select-Object -ExpandProperty ProviderPath } catch { Write-Error "无法解析路径 '$p': $($_.Exception.Message)" continue } foreach ($rp in $resolved) { $whatIf = -not $PSCmdlet.ShouldProcess($rp, "替换 SID: $SourceSid -> $TargetSid") if ($Recurse) { Invoke-RecursiveReplace -RootPath $rp -WhatIfMode $whatIf } else { $r = Invoke-SingleReplace -ResolvedPath $rp -WhatIfMode $whatIf if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() } elseif ($r.DaclAceReplaced -gt 0 -or $r.SaclAceReplaced -gt 0 -or $r.OwnerReplaced -or $r.GroupReplaced) { Write-Verbose $r.ToString() } } } } } } <# .SYNOPSIS Remove a SID's permission records from NTFS file/directory ACLs. .DESCRIPTION Remove-AclSid removes all ACE entries that match the specified SID from the DACL (Discretionary Access Control List) of each target file or directory. Two modes: -TargetSid <SID>: remove a specific SID's ACEs -AllUnknownSid: remove ACEs for any SID that cannot be resolved to a known account name (orphaned/unknown SID cleanup) Owner and Group are NOT touched. .PARAMETER Path Target file or directory path(s). Accepts pipeline input and wildcards. .PARAMETER TargetSid The SID to remove. Accepts S-1-5-... string or DOMAIN\user account name. Used in ByTargetSid parameter set. .PARAMETER AllUnknownSid Remove ACEs for all SIDs that the system cannot resolve to an account name. Used in AllUnknown parameter set. .PARAMETER AccessControlType Filter by access control type. Valid values: "Allow", "Deny". Default: both. .PARAMETER Recurse Recursively process all subdirectories and files. .PARAMETER IncludeInherited Also remove inherited ACEs (default: only explicit ACEs). .PARAMETER FollowReparsePoints Follow reparse points (symlinks, junctions) during recursion. .PARAMETER StopOnError Stop on the first error instead of continuing. .PARAMETER PassThru Output a result object for each processed entry. .EXAMPLE Remove-AclSid -Path C:\Data -TargetSid S-1-5-21-xxx-xxx-xxx-1001 -Recurse Remove all ACEs for a specific SID recursively. .EXAMPLE Remove-AclSid -Path C:\Data -AllUnknownSid -Recurse -WhatIf Preview removing all unknown SID ACEs. #> function Remove-AclSid { [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByTargetSid')] [OutputType([pscustomobject])] param( [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByTargetSid')] [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'AllUnknown')] [Alias('FullName', 'PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, [Parameter(Mandatory, Position = 1, ParameterSetName = 'ByTargetSid')] [Alias('Sid')] [string]$TargetSid, [Parameter(Mandatory, ParameterSetName = 'AllUnknown')] [switch]$AllUnknownSid, [Parameter()] [ValidateSet('Allow', 'Deny')] [string[]]$AccessControlType = @('Allow', 'Deny'), [switch]$Recurse, [switch]$IncludeInherited, [switch]$FollowReparsePoints, [switch]$StopOnError, [switch]$PassThru ) begin { # ---------- 加载 ACL 类型 ---------- $aclTypes = @( 'System.Security.AccessControl.RawSecurityDescriptor' 'System.Security.AccessControl.RawAcl' 'System.Security.AccessControl.CommonAce' 'System.Security.AccessControl.ObjectCommonAce' 'System.Security.AccessControl.AceFlags' 'System.Security.AccessControl.AceQualifier' 'System.Security.AccessControl.AccessControlSections' 'System.Security.AccessControl.DirectorySecurity' 'System.Security.AccessControl.FileSecurity' 'System.Security.Principal.SecurityIdentifier' 'System.Security.Principal.NTAccount' ) foreach ($t in $aclTypes) { if (-not ($t -as [type])) { try { Add-Type -AssemblyName 'System.IO.FileSystem.AccessControl' -ErrorAction Stop } catch {} break } } # ---------- 解析 SID ---------- function ConvertTo-SecurityIdentifier { param([string]$sid_text) if ($sid_text -match '^S-1-\d') { try { return [System.Security.Principal.SecurityIdentifier]::new($sid_text) } catch { throw "无法解析 SID 字符串 '$sid_text': $($_.Exception.Message)" } } try { $nt = [System.Security.Principal.NTAccount]::new($sid_text) return $nt.Translate([System.Security.Principal.SecurityIdentifier]) } catch { throw "无法将 '$sid_text' 解析为 SID 或账户名。" } } $allUnknown = $AllUnknownSid.IsPresent $targetId = $null if (-not $allUnknown) { try { $targetId = ConvertTo-SecurityIdentifier $TargetSid } catch { throw $_ } } Write-Verbose "移除模式: $(if ($allUnknown) { '所有未知 SID' } else { $targetId.Value })" Write-Verbose "ACE 类型过滤: $($AccessControlType -join ', ')" # ---------- 核心:从 RawAcl 中移除匹配的 ACE ---------- function Remove-SidsFromAcl { param( [System.Security.AccessControl.RawAcl]$SourceAcl, [System.Security.Principal.SecurityIdentifier]$TargetId, [bool]$AllUnknown, [bool]$IncludeInherited, [string[]]$AceTypes ) $newAcl = [System.Security.AccessControl.RawAcl]::new($SourceAcl.Revision, $SourceAcl.Count) $removed = 0 for ($i = 0; $i -lt $SourceAcl.Count; $i++) { $ace = $SourceAcl[$i] # 跳过继承的 ACE(除非显式要求) $isInherited = ($ace.AceFlags -band [System.Security.AccessControl.AceFlags]::Inherited) -ne 0 if (-not $IncludeInherited -and $isInherited) { $null = $newAcl.InsertAce($newAcl.Count, $ace) continue } # 获取 SID $sid = $null if ($ace -is [System.Security.AccessControl.CommonAce]) { $sid = $ace.SecurityIdentifier } elseif ($ace -is [System.Security.AccessControl.ObjectCommonAce]) { $sid = $ace.SecurityIdentifier } $shouldRemove = $false if ($null -ne $sid) { if ($AllUnknown) { try { $null = $sid.Translate([System.Security.Principal.NTAccount]) } catch { $shouldRemove = $true } } elseif ($null -ne $TargetId -and $sid.Value -eq $TargetId.Value) { $shouldRemove = $true } } # 按 ACE 类型(Allow / Deny)过滤 if ($shouldRemove -and $AceTypes.Length -gt 0) { $qualifier = [System.Security.AccessControl.AceQualifier]($ace.AceType -as [byte]) $matchesType = $false foreach ($at in $AceTypes) { if ($at -eq 'Allow' -and $qualifier -eq [System.Security.AccessControl.AceQualifier]::AccessAllowed) { $matchesType = $true } elseif ($at -eq 'Deny' -and $qualifier -eq [System.Security.AccessControl.AceQualifier]::AccessDenied) { $matchesType = $true } } $shouldRemove = $matchesType } if ($shouldRemove) { $removed++ } else { $null = $newAcl.InsertAce($newAcl.Count, $ace) } } return $newAcl, $removed } # ---------- 单路径处理 ---------- function Invoke-SingleRemove { param( [string]$ResolvedPath, [bool]$WhatIfMode ) $sw = [System.Diagnostics.Stopwatch]::StartNew() $result = [pscustomobject]@{ Path = $ResolvedPath EntryType = '' Success = $false Error = $null DaclAceReplaced = 0 SaclAceReplaced = 0 OwnerReplaced = $false GroupReplaced = $false Written = $false ElapsedMilliseconds = 0 } try { $isDir = [System.IO.Directory]::Exists($ResolvedPath) $isFile = [System.IO.File]::Exists($ResolvedPath) if (-not $isDir -and -not $isFile) { $result.Error = '路径不存在' return $result } $result.EntryType = if ($isDir) { 'Directory' } else { 'File' } $sections = [System.Security.AccessControl.AccessControlSections]::Owner ` -bor [System.Security.AccessControl.AccessControlSections]::Group ` -bor [System.Security.AccessControl.AccessControlSections]::Access if ($isDir) { $sd = [System.Security.AccessControl.DirectorySecurity]::new($ResolvedPath, $sections) } else { $sd = [System.Security.AccessControl.FileSecurity]::new($ResolvedPath, $sections) } $sdBytes = $sd.GetSecurityDescriptorBinaryForm() $rawSd = [System.Security.AccessControl.RawSecurityDescriptor]::new($sdBytes, 0) # 移除 DACL 中的匹配 ACE if ($null -ne $rawSd.DiscretionaryAcl) { $newAcl, $removed = Remove-SidsFromAcl ` -SourceAcl $rawSd.DiscretionaryAcl ` -TargetId $targetId ` -AllUnknown $allUnknown ` -IncludeInherited $IncludeInherited ` -AceTypes $AccessControlType $result.DaclAceReplaced = $removed $rawSd.DiscretionaryAcl = $newAcl } # Owner / Group 不处理 $hasChanges = $result.DaclAceReplaced -gt 0 if (-not $hasChanges) { $result.Success = $true $result.Written = $false return $result } if ($WhatIfMode) { $result.Success = $true $result.Written = $false return $result } # 写回 $newSdBytes = New-Object byte[] $rawSd.BinaryLength $rawSd.GetBinaryForm($newSdBytes, 0) if ($isDir) { $newSd = [System.Security.AccessControl.DirectorySecurity]::new() $newSd.SetSecurityDescriptorBinaryForm($newSdBytes) Set-Acl -Path $ResolvedPath -AclObject $newSd } else { $newSd = [System.Security.AccessControl.FileSecurity]::new() $newSd.SetSecurityDescriptorBinaryForm($newSdBytes) Set-Acl -Path $ResolvedPath -AclObject $newSd } $result.Success = $true $result.Written = $true } catch [System.UnauthorizedAccessException] { $result.Error = "访问被拒绝(需要管理员权限): $($_.Exception.Message)" } catch { $result.Error = $_.Exception.Message } finally { $sw.Stop() $result.ElapsedMilliseconds = $sw.ElapsedMilliseconds } return $result } # ---------- 递归遍历 ---------- function Invoke-RecursiveRemove { param([string]$RootPath, [bool]$WhatIfMode) $r = Invoke-SingleRemove -ResolvedPath $RootPath -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0) { Write-Verbose $r.ToString() } if (-not $Recurse) { return } if (-not [System.IO.Directory]::Exists($RootPath)) { return } $stack = [System.Collections.Generic.Stack[string]]::new() $stack.Push($RootPath) while ($stack.Count -gt 0) { $current = $stack.Pop() $dirs = @() try { $dirs = [System.IO.Directory]::EnumerateDirectories($current, '*', [System.IO.SearchOption]::TopDirectoryOnly) } catch { continue } foreach ($dir in $dirs) { if (-not $FollowReparsePoints) { try { $attrs = [System.IO.File]::GetAttributes($dir) if (($attrs -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { continue } } catch {} } $r = Invoke-SingleRemove -ResolvedPath $dir -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0) { Write-Verbose $r.ToString() } $stack.Push($dir) } $files = @() try { $files = [System.IO.Directory]::EnumerateFiles($current, '*', [System.IO.SearchOption]::TopDirectoryOnly) } catch { continue } foreach ($file in $files) { $r = Invoke-SingleRemove -ResolvedPath $file -WhatIfMode $WhatIfMode if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() if ($StopOnError) { return } } elseif ($r.DaclAceReplaced -gt 0) { Write-Verbose $r.ToString() } } } } } process { foreach ($p in $Path) { $resolved = @() try { $resolved = Resolve-Path -Path $p -ErrorAction Stop | Select-Object -ExpandProperty ProviderPath } catch { Write-Error "无法解析路径 '$p': $($_.Exception.Message)" continue } foreach ($rp in $resolved) { $sidDesc = if ($allUnknown) { "所有未知 SID" } else { "$TargetSid" } $whatIf = -not $PSCmdlet.ShouldProcess($rp, "移除 SID $sidDesc 的所有 ACE") if ($Recurse) { Invoke-RecursiveRemove -RootPath $rp -WhatIfMode $whatIf } else { $r = Invoke-SingleRemove -ResolvedPath $rp -WhatIfMode $whatIf if ($PassThru) { Write-Output $r } if (-not $r.Success) { Write-Warning $r.ToString() } elseif ($r.DaclAceReplaced -gt 0) { Write-Verbose $r.ToString() } } } } } } # 如果直接运行脚本而非 dot-source,支持 -Path 参数调用 # 用法示例: # . .\Update-AclSid.ps1 # Update-AclSid -Path C:\Data -SourceSid S-1-5-21-... -TargetSid S-1-5-21-... -Recurse -WhatIf # Remove-AclSid -Path C:\Data -TargetSid S-1-5-21-... -Recurse # Remove-AclSid -Path C:\Data -AllUnknownSid -Recurse -WhatIf |