Powershell/Private/Permissions/Set-RegPermission.ps1
|
function Set-RegPermission { param ( [Parameter(Mandatory)] [string]$SourceSID, [Parameter(Mandatory)] [string]$TargetSID, [Parameter(Mandatory)] [string]$FilePath, [switch]$Recursive, [int]$ProgressHeartbeatIntervalSeconds = 60, [scriptblock]$OnProgressHeartbeat, [int]$MaxThreads = 4 ) # --------------------------------------------------------------------------- # Embed NativeAcl C# Class (Compiled once per AppDomain) # --------------------------------------------------------------------------- if (-not ([System.Management.Automation.PSTypeName]'NativeAcl').Type) { Add-Type -Language CSharp -TypeDefinition @' using System; using System.Collections.Generic; using System.Runtime.InteropServices; public static class NativeAcl { public const uint SE_FILE_OBJECT = 1; public const uint OWNER_SECURITY_INFORMATION = 0x00000001; public const uint DACL_SECURITY_INFORMATION = 0x00000004; public const uint PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000; public const uint FILE_ALL_ACCESS = 0x001F01FF; public const uint GRANT_ACCESS = 1; public const uint NO_MULTIPLE_TRUSTEE = 0; public const uint TRUSTEE_IS_SID = 0; public const uint TRUSTEE_IS_UNKNOWN = 0; public const uint SUB_CONTAINERS_AND_OBJECTS_INHERIT = 0x3; public const uint TREE_SEC_INFO_SET = 0x00000001; public const uint PROG_INVOKE_ON_ERROR = 3; public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; public const uint TOKEN_QUERY = 0x0008; public const uint SE_PRIVILEGE_ENABLED = 0x00000002; [StructLayout(LayoutKind.Sequential)] public struct LUID { public uint LowPart; public int HighPart; } [StructLayout(LayoutKind.Sequential)] public struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_PRIVILEGES_1 { public uint PrivilegeCount; public LUID_AND_ATTRIBUTES Privilege; } [StructLayout(LayoutKind.Sequential)] public struct TRUSTEE_W { public IntPtr pMultipleTrustee; public uint MultipleTrusteeOperation; public uint TrusteeForm; public uint TrusteeType; public IntPtr ptstrName; } [StructLayout(LayoutKind.Sequential)] public struct EXPLICIT_ACCESS_W { public uint grfAccessPermissions; public uint grfAccessMode; public uint grfInheritance; public TRUSTEE_W Trustee; } public delegate void FN_PROGRESS( IntPtr pObjectName, uint status, IntPtr pInvokeSetting, IntPtr args, [MarshalAs(UnmanagedType.Bool)] bool securitySet); [DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll")] public static extern IntPtr LocalFree(IntPtr hMem); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool AdjustTokenPrivileges( IntPtr TokenHandle, [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES_1 NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetEntriesInAclW")] public static extern uint SetEntriesInAcl( uint cCountOfExplicitEntries, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] EXPLICIT_ACCESS_W[] pListOfExplicitEntries, IntPtr OldAcl, out IntPtr NewAcl); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetNamedSecurityInfoW")] public static extern uint SetNamedSecurityInfo( string pObjectName, uint ObjectType, uint SecurityInfo, IntPtr psidOwner, IntPtr psidGroup, IntPtr pDacl, IntPtr pSacl); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "TreeSetNamedSecurityInfoW")] public static extern uint TreeSetNamedSecurityInfo( string pObjectName, uint ObjectType, uint SecurityInfo, IntPtr psidOwner, IntPtr psidGroup, IntPtr pDacl, IntPtr pSacl, uint dwAction, FN_PROGRESS fnProgress, uint ProgressInvokeSetting, IntPtr Args); public static IntPtr SidToUnmanaged(byte[] sidBytes) { IntPtr ptr = Marshal.AllocHGlobal(sidBytes.Length); Marshal.Copy(sidBytes, 0, ptr, sidBytes.Length); return ptr; } private static void EnableSinglePrivilege(IntPtr token, string name) { LUID id; if (!LookupPrivilegeValueW(null, name, out id)) throw new InvalidOperationException(string.Format("LookupPrivilegeValue(\"{0}\"): error {1}", name, Marshal.GetLastWin32Error())); var tp = new TOKEN_PRIVILEGES_1 { PrivilegeCount = 1, Privilege = new LUID_AND_ATTRIBUTES { Luid = id, Attributes = SE_PRIVILEGE_ENABLED } }; bool ok = AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); int err = Marshal.GetLastWin32Error(); if (!ok) throw new InvalidOperationException(string.Format("AdjustTokenPrivileges(\"{0}\"): error {1}", name, err)); } public static void EnablePrivileges() { IntPtr token; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out token)) throw new InvalidOperationException(string.Format("OpenProcessToken: error {0}", Marshal.GetLastWin32Error())); try { EnableSinglePrivilege(token, "SeRestorePrivilege"); EnableSinglePrivilege(token, "SeTakeOwnershipPrivilege"); } finally { CloseHandle(token); } } public struct FailedItem { public string Path; public uint ErrorCode; } private static IntPtr BuildGrantDacl(IntPtr userPtr, IntPtr systemPtr, IntPtr adminsPtr) { var entries = new EXPLICIT_ACCESS_W[3]; // ACE 0: SYSTEM entries[0].grfAccessPermissions = FILE_ALL_ACCESS; entries[0].grfAccessMode = GRANT_ACCESS; entries[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; entries[0].Trustee.pMultipleTrustee = IntPtr.Zero; entries[0].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; entries[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; entries[0].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; entries[0].Trustee.ptstrName = systemPtr; // ACE 1: Administrators entries[1].grfAccessPermissions = FILE_ALL_ACCESS; entries[1].grfAccessMode = GRANT_ACCESS; entries[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; entries[1].Trustee.pMultipleTrustee = IntPtr.Zero; entries[1].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; entries[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; entries[1].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; entries[1].Trustee.ptstrName = adminsPtr; // ACE 2: Target User entries[2].grfAccessPermissions = FILE_ALL_ACCESS; entries[2].grfAccessMode = GRANT_ACCESS; entries[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; entries[2].Trustee.pMultipleTrustee = IntPtr.Zero; entries[2].Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; entries[2].Trustee.TrusteeForm = TRUSTEE_IS_SID; entries[2].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; entries[2].Trustee.ptstrName = userPtr; IntPtr dacl; uint ret = SetEntriesInAcl(3, entries, IntPtr.Zero, out dacl); if (ret != 0) throw new InvalidOperationException(string.Format("SetEntriesInAcl: error {0}", ret)); return dacl; } // Stamps owner + protected DACL on a single object without walking its children. Callers // that already processed the children (the recursive path fans them out across threads) // use this so the container ends up with the same ACL the tree walk would have given it. public static void ApplyOwnerAndGrantSelf(string root, byte[] userSidBytes, byte[] systemSidBytes, byte[] adminsSidBytes) { IntPtr userPtr = SidToUnmanaged(userSidBytes); IntPtr systemPtr = SidToUnmanaged(systemSidBytes); IntPtr adminsPtr = SidToUnmanaged(adminsSidBytes); IntPtr dacl = IntPtr.Zero; try { dacl = BuildGrantDacl(userPtr, systemPtr, adminsPtr); uint r = SetNamedSecurityInfo(root, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, userPtr, IntPtr.Zero, dacl, IntPtr.Zero); if (r != 0) throw new InvalidOperationException(string.Format("SetNamedSecurityInfo (protect root) \"{0}\": error {1}", root, r)); } finally { if (dacl != IntPtr.Zero) LocalFree(dacl); Marshal.FreeHGlobal(userPtr); Marshal.FreeHGlobal(systemPtr); Marshal.FreeHGlobal(adminsPtr); } } public static FailedItem[] ApplyOwnerAndGrantTree(string root, byte[] userSidBytes, byte[] systemSidBytes, byte[] adminsSidBytes) { // Failures are collected per invocation so concurrent callers (the recursive path runs this // across a runspace pool) never share or clear each other's results. List<FailedItem> failedPaths = new List<FailedItem>(); FN_PROGRESS progressDelegate = delegate(IntPtr pObjectName, uint status, IntPtr pInvokeSetting, IntPtr args, bool securitySet) { if (status != 0) { string path = Marshal.PtrToStringUni(pObjectName) ?? "Unknown Path"; lock (failedPaths) { failedPaths.Add(new FailedItem { Path = path, ErrorCode = status }); } } }; IntPtr userPtr = SidToUnmanaged(userSidBytes); IntPtr systemPtr = SidToUnmanaged(systemSidBytes); IntPtr adminsPtr = SidToUnmanaged(adminsSidBytes); IntPtr dacl = IntPtr.Zero; try { dacl = BuildGrantDacl(userPtr, systemPtr, adminsPtr); uint r1 = TreeSetNamedSecurityInfo(root, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, userPtr, IntPtr.Zero, dacl, IntPtr.Zero, TREE_SEC_INFO_SET, progressDelegate, PROG_INVOKE_ON_ERROR, IntPtr.Zero); GC.KeepAlive(progressDelegate); int failureCount; lock (failedPaths) { failureCount = failedPaths.Count; } if (r1 != 0 && failureCount == 0) throw new InvalidOperationException(string.Format("TreeSetNamedSecurityInfo \"{0}\": error {1}", root, r1)); uint r2 = SetNamedSecurityInfo(root, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, userPtr, IntPtr.Zero, dacl, IntPtr.Zero); if (r2 != 0) throw new InvalidOperationException(string.Format("SetNamedSecurityInfo (protect root) \"{0}\": error {1}", root, r2)); lock (failedPaths) { return failedPaths.ToArray(); } } finally { if (dacl != IntPtr.Zero) LocalFree(dacl); Marshal.FreeHGlobal(userPtr); Marshal.FreeHGlobal(systemPtr); Marshal.FreeHGlobal(adminsPtr); } } } '@ } # --------------------------------------------------------------------------- # Local Helper Functions (Restored for icacls fallback) # --------------------------------------------------------------------------- function local:Get-IcaclsProcessExitCode { param([Parameter(Mandatory = $true)][System.Diagnostics.Process]$Process) if (-not $Process.HasExited) { $Process.WaitForExit() | Out-Null } $Process.Refresh() $exitCode = $Process.ExitCode if ($null -eq $exitCode) { return 0 } return [int]$exitCode } function local:Invoke-IcaclsWithHeartbeat { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string[]]$Arguments, [int]$HeartbeatIntervalSeconds, [scriptblock]$OnHeartbeat ) $local:ErrorActionPreference = 'Continue' $argumentList = @($Path) + $Arguments $process = Start-Process -FilePath 'icacls.exe' -ArgumentList $argumentList -PassThru -NoNewWindow -Wait:$false if ($HeartbeatIntervalSeconds -gt 0 -and $OnHeartbeat) { $intervalMs = [math]::Max(1, $HeartbeatIntervalSeconds) * 1000 while (-not $process.HasExited) { if ($process.WaitForExit($intervalMs)) { break } & $OnHeartbeat } } $script:IcaclsExitCode = Get-IcaclsProcessExitCode -Process $process $process.Dispose() return @() } # --------------------------------------------------------------------------- # Main Function Logic # --------------------------------------------------------------------------- if ([string]::IsNullOrWhiteSpace($FilePath)) { throw 'Set-RegPermission requires a non-empty FilePath.' } if (-not (Test-Path -LiteralPath $FilePath)) { throw "Set-RegPermission path does not exist: $FilePath" } $FilePath = [System.IO.Path]::GetFullPath($FilePath) $script:IcaclsExitCode = 0 $ntfsPermissionLogPath = Join-Path $(if (-not [string]::IsNullOrWhiteSpace($env:SystemDrive)) { $env:SystemDrive } else { 'C:' }) 'Windows\Temp\jcAdmu.log' $SourceSIDObj = New-Object System.Security.Principal.SecurityIdentifier($SourceSID) $TargetSIDObj = New-Object System.Security.Principal.SecurityIdentifier($TargetSID) $SourceAccountTranslated = $false $TargetAccountTranslated = $false try { $SourceAccount = $SourceSIDObj.Translate([System.Security.Principal.NTAccount]).Value $SourceAccountTranslated = $true } catch { $SourceAccount = $SourceSID } try { $TargetAccount = $TargetSIDObj.Translate([System.Security.Principal.NTAccount]).Value $TargetAccountTranslated = $true } catch { Write-ToLog "Could not translate TargetSID $TargetSID to NTAccount. Using SID string instead." -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath $TargetAccount = $TargetSID } if (-not $SourceAccountTranslated) { Write-ToLog "Could not translate SourceSID $SourceSID to NTAccount. Using SID string instead." -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } Write-ToLog "Starting permission update on '$FilePath' (Recursive=$Recursive) from $SourceAccount to $TargetAccount" -Step "Set-RegPermission" -Path $ntfsPermissionLogPath if ($Recursive) { # ========================================================================= # RECURSIVE: Use C# P/Invoke via background Runspace for max performance # ========================================================================= $attrs = [System.IO.File]::GetAttributes($FilePath) if ($attrs.HasFlag([System.IO.FileAttributes]::ReparsePoint)) { Write-ToLog "Skipping '$FilePath': Root path is a reparse point (symlink or junction). Refusing to follow natively." -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath return } try { [NativeAcl]::EnablePrivileges() $targetSidBytes = New-Object byte[] $TargetSIDObj.BinaryLength $TargetSIDObj.GetBinaryForm($targetSidBytes, 0) $systemSidObj = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-18') $systemSidBytes = New-Object byte[] $systemSidObj.BinaryLength $systemSidObj.GetBinaryForm($systemSidBytes, 0) $adminSidObj = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') $adminSidBytes = New-Object byte[] $adminSidObj.BinaryLength $adminSidObj.GetBinaryForm($adminSidBytes, 0) # Create an isolated background runspace inside the same process $runspace = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $runspace.Open() $subDirs = [System.IO.Directory]::EnumerateDirectories($FilePath) $jobs = @() $failedItems = @() $useProgressHeartbeat = $ProgressHeartbeatIntervalSeconds -gt 0 -and $null -ne $OnProgressHeartbeat $heartbeatIntervalMs = if ($useProgressHeartbeat) { [math]::Max(1, $ProgressHeartbeatIntervalSeconds) * 1000 } else { 0 } $heartbeatStopwatch = if ($useProgressHeartbeat) { [System.Diagnostics.Stopwatch]::StartNew() } else { $null } # Queue background threads for sub-directories in parallel foreach ($dir in $subDirs) { $ps = [powershell]::Create().AddScript({ param($path, $tgtBytes, $sysBytes, $admBytes) return [NativeAcl]::ApplyOwnerAndGrantTree($path, $tgtBytes, $sysBytes, $admBytes) }).AddArgument($dir).AddArgument($targetSidBytes).AddArgument($systemSidBytes).AddArgument($adminSidBytes) $ps.RunspacePool = $runspace $asyncResult = $ps.BeginInvoke() $jobs += [PSCustomObject]@{ Path = $dir Pipe = $ps Status = $asyncResult } # Manage active queue limit & fire UI Heartbeats on the configured interval $activeJobs = @($jobs | Where-Object { $_.Status.IsCompleted -eq $false }) while ($activeJobs.Count -ge $MaxThreads) { if ($useProgressHeartbeat -and $heartbeatStopwatch.ElapsedMilliseconds -ge $heartbeatIntervalMs) { & $OnProgressHeartbeat $heartbeatStopwatch.Restart() } Start-Sleep -Milliseconds 200 $activeJobs = @($jobs | Where-Object { $_.Status.IsCompleted -eq $false }) } } # Await the completion of all outstanding queues & fire UI Heartbeats on the configured interval $activeJobs = @($jobs | Where-Object { $_.Status.IsCompleted -eq $false }) while ($activeJobs.Count -gt 0) { if ($useProgressHeartbeat -and $heartbeatStopwatch.ElapsedMilliseconds -ge $heartbeatIntervalMs) { & $OnProgressHeartbeat $heartbeatStopwatch.Restart() } Start-Sleep -Milliseconds 200 $activeJobs = @($jobs | Where-Object { $_.Status.IsCompleted -eq $false }) } # Harvest Thread Errors & Dispose Pools foreach ($job in $jobs) { $res = $job.Pipe.EndInvoke($job.Status) if ($null -ne $res) { $failedItems += $res } $job.Pipe.Dispose() } $runspace.Close() $runspace.Dispose() # Sub-directories were applied in parallel, but root files were excluded. Process them # via Invoke-NativeTreeAcl so tests can mock failed-item results. foreach ($file in [System.IO.Directory]::EnumerateFiles($FilePath)) { $res = Invoke-NativeTreeAcl -Root $file -TargetSidBytes $targetSidBytes -SystemSidBytes $systemSidBytes -AdminSidBytes $adminSidBytes if ($null -ne $res) { $failedItems += $res } } [NativeAcl]::ApplyOwnerAndGrantSelf($FilePath, $targetSidBytes, $systemSidBytes, $adminSidBytes) # Evaluate any items that could not be processed natively if ($failedItems.Count -gt 0) { $symlinkCount = 0 $errorCount = 0 foreach ($item in $failedItems) { try { $itemAttrs = [System.IO.File]::GetAttributes($item.Path) if ($itemAttrs.HasFlag([System.IO.FileAttributes]::ReparsePoint)) { $symlinkCount++ Write-ToLog "Skipped '$($item.Path)': item is a symlink/reparse point, this is expected and can be ignored." -Level Info -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } else { $errorCount++ Write-ToLog "Failed to set permissions on '$($item.Path)': Win32 error $($item.ErrorCode)." -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } } catch { $errorCount++ Write-ToLog "Failed to set permissions on '$($item.Path)': Win32 error $($item.ErrorCode). Unable to determine reparse point status: $($_.Exception.Message)" -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } } Write-ToLog "Native tree operation completed with $($failedItems.Count) skipped/failed items ($symlinkCount symlinks, $errorCount other errors)." -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } } catch { Write-ToLog "Error natively stamping tree ACL: $($_.Exception.Message)" -Level Error -Step "Set-RegPermission" -Path $ntfsPermissionLogPath throw } } else { # ========================================================================= # NON-RECURSIVE: Fallback to existing icacls logic for Root/Scheduled Task prep # ========================================================================= $useProgressHeartbeat = $ProgressHeartbeatIntervalSeconds -gt 0 -and $null -ne $OnProgressHeartbeat $TargetAccountIcacls = if ($TargetAccountTranslated) { $TargetAccount } else { "*$TargetAccount" } Write-ToLog "Preparing root-level ACL for '$FilePath' (target: $TargetAccountIcacls)." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath $acl = Get-Acl -LiteralPath $FilePath $targetMember = $acl.Access | Where-Object { $_.IdentityReference -eq $TargetAccount } if (-not $targetMember) { Write-ToLog "Adding FullControl ACE for $TargetAccount on root path." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath $newRule = New-Object System.Security.AccessControl.FileSystemAccessRule( $TargetAccount, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" ) $acl.AddAccessRule($newRule) Set-Acl -LiteralPath $FilePath -AclObject $acl } else { Write-ToLog "Target account $TargetAccount already has an ACE on root path." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } $grantArguments = @('/grant', "${TargetAccountIcacls}:(OI)(CI)F", '/C', '/Q') $ownerArguments = @('/setowner', "$TargetAccountIcacls", '/C', '/Q') # Step 1: Grant target user Write-ToLog "Granting FullControl to $TargetAccountIcacls on '$FilePath'." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath if ($useProgressHeartbeat) { Invoke-IcaclsWithHeartbeat -Path $FilePath -Arguments $grantArguments -HeartbeatIntervalSeconds $ProgressHeartbeatIntervalSeconds -OnHeartbeat $OnProgressHeartbeat | Out-Null } else { & icacls.exe $FilePath $grantArguments 2>&1 | Out-Null $script:IcaclsExitCode = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 } } if ($script:IcaclsExitCode -ne 0) { Write-ToLog "icacls grant completed with exit code: $script:IcaclsExitCode" -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } else { Write-ToLog "Successfully granted FullControl to $TargetAccountIcacls." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } # Step 2: Change ownership Write-ToLog "Setting owner to $TargetAccountIcacls on '$FilePath'." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath if ($useProgressHeartbeat) { Invoke-IcaclsWithHeartbeat -Path $FilePath -Arguments $ownerArguments -HeartbeatIntervalSeconds $ProgressHeartbeatIntervalSeconds -OnHeartbeat $OnProgressHeartbeat | Out-Null } else { & icacls.exe $FilePath $ownerArguments 2>&1 | Out-Null $script:IcaclsExitCode = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 } } if ($script:IcaclsExitCode -ne 0) { Write-ToLog "icacls setowner completed with exit code: $script:IcaclsExitCode" -Level Warning -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } else { Write-ToLog "Successfully set owner to $TargetAccountIcacls." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } Write-ToLog "Root-level permission update completed for '$FilePath'." -Step "Set-RegPermission" -Path $ntfsPermissionLogPath } } |