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 = 0,
        [scriptblock]$OnProgressHeartbeat
    )

    # ---------------------------------------------------------------------------
    # Embed NativeAcl C# Class (Compiled once per runspace)
    # ---------------------------------------------------------------------------
    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 static List<string> FailedPaths = new List<string>();
    private static FN_PROGRESS _progressDelegate;
 
    private static void ProgressCallback(IntPtr pObjectName, uint status, IntPtr pInvokeSetting, IntPtr args, bool securitySet)
    {
        if (status != 0) {
            string path = Marshal.PtrToStringUni(pObjectName) ?? "Unknown Path";
            FailedPaths.Add(path + " (Win32 Error: " + status + ")");
        }
    }
 
    public static string[] ApplyOwnerAndGrantTree(string root, byte[] userSidBytes, byte[] systemSidBytes, byte[] adminsSidBytes)
    {
        FailedPaths.Clear();
        IntPtr userPtr = SidToUnmanaged(userSidBytes);
        IntPtr systemPtr = SidToUnmanaged(systemSidBytes);
        IntPtr adminsPtr = SidToUnmanaged(adminsSidBytes);
        IntPtr dacl = IntPtr.Zero;
        try {
            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;
 
            uint ret = SetEntriesInAcl(3, entries, IntPtr.Zero, out dacl);
            if (ret != 0) throw new InvalidOperationException(string.Format("SetEntriesInAcl: error {0}", ret));
 
            _progressDelegate = ProgressCallback;
 
            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);
 
            if (r1 != 0 && FailedPaths.Count == 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));
 
            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 {
        $TargetAccount = $TargetSID
    }

    if ($Recursive) {
        # =========================================================================
        # RECURSIVE: Use C# P/Invoke for maximum 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
            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)

            $failedItems = [NativeAcl]::ApplyOwnerAndGrantTree($FilePath, $targetSidBytes, $systemSidBytes, $adminSidBytes)

            if ($failedItems.Count -gt 0) {
                Write-ToLog "Native tree operation completed with $($failedItems.Count) skipped locked files." -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

        $SourceAccountIcacls = if ($SourceAccountTranslated) { $SourceAccount } else { "*$SourceAccount" }
        $TargetAccountIcacls = if ($TargetAccountTranslated) { $TargetAccount } else { "*$TargetAccount" }

        $acl = Get-Acl -LiteralPath $FilePath
        $targetMember = $acl.Access | Where-Object { $_.IdentityReference -eq $TargetAccount }
        if (-not $targetMember) {
            $newRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
                $TargetAccount, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
            )
            $acl.AddAccessRule($newRule)
            Set-Acl -LiteralPath $FilePath -AclObject $acl
        }

        $grantArguments = @('/grant', "${TargetAccountIcacls}:(OI)(CI)F", '/C', '/Q')
        $ownerArguments = @('/setowner', "$TargetAccountIcacls", '/C', '/Q')

        # Step 1: Grant target user
        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 }
        }

        # Step 2: Change ownership
        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 }
        }
    }
}