SddlUtils.ps1

function Get-AllAceFlagsMatch {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Security.AccessControl.RawSecurityDescriptor]$SecurityDescriptor,
        
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.AceFlags]$EnabledFlags,

        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.AceFlags]$DisabledFlags
    )

    process {
        foreach ($ace in $SecurityDescriptor.DiscretionaryAcl) {
            $hasAllBitsFromEnabledFlags = ([int]$ace.AceFlags -band [int]$EnabledFlags) -eq [int]$EnabledFlags
            $hasNoBitsFromDisabledFlags = ([int]$ace.AceFlags -band [int]$DisabledFlags) -eq 0
            if (-not ($hasAllBitsFromEnabledFlags -and $hasNoBitsFromDisabledFlags)) {
                return $false
            }
        }

        return $true
    }
}

function Set-AceFlags {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "We are setting the AceFlags property.")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        "PSUseShouldProcessForStateChangingFunctions",
        "",
        Justification = "No external side effects, just changes the security descriptor object in-place. So no real value to supporting -WhatIf.")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Security.AccessControl.RawSecurityDescriptor]$SecurityDescriptor,
        
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.AceFlags]$EnableFlags,

        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.AceFlags]$DisableFlags
    )

    begin {
        if ([int]$EnableFlags -band [int]$DisableFlags) {
            throw "Enable and disable flags cannot overlap"
        }
    }

    process {
        # Create new ACEs with updated flags
        $newAces = $SecurityDescriptor.DiscretionaryAcl | ForEach-Object {
            $aceFlags = ([int]$_.AceFlags -bor [int]$EnableFlags) -band (-bnot [int]$DisableFlags)

            if ($_.GetType().Name -eq "CommonAce") {
                [System.Security.AccessControl.CommonAce]::new(
                    $aceFlags,
                    $_.AceQualifier,
                    $_.AccessMask,
                    $_.SecurityIdentifier,
                    $_.IsCallback,
                    $_.GetOpaque())
            }
            else {
                throw "Unsupported ACE type: $($_.GetType().Name)"
            }
        }
        
        # Remove all old ACEs
        for ($i = $SecurityDescriptor.DiscretionaryAcl.Count - 1; $i -ge 0; $i--) {
            $SecurityDescriptor.DiscretionaryAcl.RemoveAce($i) | Out-Null
        }
        
        # Add all new ACEs
        for ($i = 0; $i -lt $newAces.Count; $i++) {
            $SecurityDescriptor.DiscretionaryAcl.InsertAce($i, $newAces[$i]) | Out-Null
        }
    }
}

<#
.SYNOPSIS
Object-specific rights for files and folders.
#>

enum SpecificRights {
    FILE_READ_DATA = 0x1
    FILE_LIST_DIRECTORY = 0x1
    FILE_WRITE_DATA = 0x2
    FILE_ADD_FILE = 0x2
    FILE_APPEND_DATA = 0x4
    FILE_ADD_SUBDIRECTORY = 0x4
    FILE_READ_EA = 0x8
    FILE_WRITE_EA = 0x10
    FILE_EXECUTE = 0x20
    FILE_TRAVERSE = 0x20
    FILE_DELETE_CHILD = 0x40
    FILE_READ_ATTRIBUTES = 0x80
    FILE_WRITE_ATTRIBUTES = 0x100
}

<#
.SYNOPSIS
Standard rights for any type of securable object (including files and folders).
#>

enum StandardRights {
    DELETE = 0x00010000
    READ_CONTROL = 0x00020000
    WRITE_DAC = 0x00040000
    WRITE_OWNER = 0x00080000
    SYNCHRONIZE = 0x00100000
}

<#
.SYNOPSIS
Standard rights for any type of securable object (including files and folders).
#>

enum GenericRights {
    GENERIC_READ = 0x80000000
    GENERIC_WRITE = 0x40000000
    GENERIC_EXECUTE = 0x20000000
    GENERIC_ALL = 0x10000000
}

<#
.SYNOPSIS
These are the basic permissions, as displayed by the Windows File Explorer.
We have also been calling these "composite rights".
#>

enum BasicPermissions {
    # 278 is obtained via:
    # [SpecificRights]::FILE_WRITE_DATA -bor
    # [SpecificRights]::FILE_APPEND_DATA -bor
    # [SpecificRights]::FILE_WRITE_EA -bor
    # [SpecificRights]::FILE_WRITE_ATTRIBUTES
    WRITE = 278

    # 131209 is obtained via:
    # [SpecificRights]::FILE_READ_DATA -bor
    # [SpecificRights]::FILE_READ_EA -bor
    # [SpecificRights]::FILE_READ_ATTRIBUTES -bor
    # [StandardRights]::READ_CONTROL
    READ = 131209
    
    # 131241 is obtained via:
    # [BasicPermissions]::READ -bor [SpecificRights]::FILE_EXECUTE
    READ_AND_EXECUTE = 131241

    # 197055 is obtained via:
    # [BasicPermissions]::READ_AND_EXECUTE -bor
    # [BasicPermissions]::WRITE -bor
    # [StandardRights]::DELETE
    MODIFY = 197055

    # 2032127 is obtained via:
    # [BasicPermissions]::MODIFY -bor
    # [SpecificRights]::FILE_DELETE_CHILD -bor
    # [StandardRights]::WRITE_DAC -bor
    # [StandardRights]::WRITE_OWNER -bor
    # [StandardRights]::SYNCHRONIZE
    FULL_CONTROL = 2032127
}

<#
.SYNOPSIS
Standard rights combinations for any type of securable object (including files and folders).
 
.LINK
https://learn.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights
#>

enum StandardRightsCombination {
    # 2031616 is obtained via:
    # [StandardRights]::DELETE -bor
    # [StandardRights]::READ_CONTROL -bor
    # [StandardRights]::WRITE_DAC -bor
    # [StandardRights]::WRITE_OWNER -bor
    # [StandardRights]::SYNCHRONIZE
    STANDARD_RIGHTS_ALL = 2031616
    STANDARD_RIGHTS_EXECUTE = [StandardRights]::READ_CONTROL
    STANDARD_RIGHTS_READ = [StandardRights]::READ_CONTROL
    # 983040 is obtained via:
    # [StandardRights]::DELETE -bor
    # [StandardRights]::READ_CONTROL -bor
    # [StandardRights]::WRITE_DAC -bor
    # [StandardRights]::WRITE_OWNER
    STANDARD_RIGHTS_REQUIRED = 983040
    STANDARD_RIGHTS_WRITE = [StandardRights]::READ_CONTROL
}

<#
.SYNOPSIS
This is a mapping of the generic rights to the specific rights for files and folders.
 
.LINK
https://learn.microsoft.com/en-us/windows/win32/fileio/file-security-and-access-rights
#>

enum FileGenericRightsMapping {
    # FILE_GENERIC_READ is defined as the following, which evaluates to 1179785:
    #
    # [SpecificRights]::FILE_READ_ATTRIBUTES -bor
    # [SpecificRights]::FILE_READ_DATA -bor
    # [SpecificRights]::FILE_READ_EA -bor
    # [StandardRightsCombination]::STANDARD_RIGHTS_READ -bor
    # [StandardRights]::SYNCHRONIZE
    FILE_GENERIC_READ = 1179785

    # FILE_GENERIC_WRITE is defined as the following, which evaluates to 1179926:
    #
    # [SpecificRights]::FILE_APPEND_DATA -bor
    # [SpecificRights]::FILE_WRITE_ATTRIBUTES -bor
    # [SpecificRights]::FILE_WRITE_DATA -bor
    # [SpecificRights]::FILE_WRITE_EA -bor
    # [StandardRightsCombination]::STANDARD_RIGHTS_WRITE -bor
    # [StandardRights]::SYNCHRONIZE
    FILE_GENERIC_WRITE = 1179926

    # FILE_GENERIC_EXECUTE is defined as the following, which evaluates to 1179808:
    # [SpecificRights]::FILE_EXECUTE -bor
    # [SpecificRights]::FILE_READ_ATTRIBUTES -bor
    # [StandardRightsCombination]::STANDARD_RIGHTS_EXECUTE -bor
    # [StandardRights]::SYNCHRONIZE
    FILE_GENERIC_EXECUTE = 1179808

    # FILE_ALL_ACCESS is not documented, but in practice it's the same as FULL_ACCESS
    FILE_ALL_ACCESS = 2032127
}

class AccessMask {
    [int]$Value

    AccessMask([int]$mask) {
        $this.Value = $this.Normalize($mask)
    }

    [int]Normalize([int]$mask) {
        return $mask -band 0xFFFFFFFF
    }

    [bool]Has([int]$permission) {
        return ($this.Value -band $permission) -eq $permission
    }

    [Void]Add([int]$permission) {
        $this.Value = $this.Value -bor $permission
    }

    [Void]Remove([int]$permission) {
        $this.Value = $this.Value -band -bnot $permission
    }
}


function Write-SecurityDescriptor {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Security.AccessControl.RawSecurityDescriptor]$descriptor
    )

    process {
        $controlFlagsHex = "0x{0:X}" -f [int]$descriptor.ControlFlags
        
        Write-Host "Owner: $($PSStyle.Foreground.Cyan)$($descriptor.Owner)$($PSStyle.Reset)"
        Write-Host "Group: $($PSStyle.Foreground.Cyan)$($descriptor.Group)$($PSStyle.Reset)"
        Write-Host "ControlFlags: $($PSStyle.Foreground.Cyan)$controlFlagsHex$($PSStyle.Reset) ($($descriptor.ControlFlags))"
        Write-Host "DiscretionaryAcl:"
        Write-Acl $descriptor.DiscretionaryAcl -indent 4
        Write-Host "SystemAcl:"
        Write-Acl $descriptor.SystemAcl -indent 4
    }
}

function Write-Acl {
    param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [System.Security.AccessControl.RawAcl]$acl,

        [Parameter(Mandatory = $false)]
        [int]$indent = 0
    )

    begin {
        $spaces = " " * $indent
    }

    process {
        if ($acl -eq $null) {
            Write-Host "${spaces}Not present"
            return
        }

        Write-Host "${spaces}Revision: $($($PSStyle.Foreground.Cyan))$($acl.Revision)$($PSStyle.Reset)"
        Write-Host "${spaces}BinaryLength: $($($PSStyle.Foreground.Cyan))$($acl.BinaryLength)$($PSStyle.Reset)"
        Write-Host "${spaces}AceCount: $($($PSStyle.Foreground.Cyan))$($acl.Count)$($PSStyle.Reset)"
        $i = 0
        foreach ($ace in $acl) {
            Write-Host "${spaces}Ace $($PSStyle.Foreground.Green)${i}$($PSStyle.Reset):"
            Write-Ace $ace -indent ($indent + 4)
            $i++
        }
    }
}

function Write-Ace {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Security.AccessControl.GenericAce]$ace,

        [Parameter(Mandatory = $false)]
        [int]$indent = 0
    )

    begin {
        $spaces = " " * $indent
    }

    process {
        $aceTypeHex = "0x{0:X}" -f [int]$ace.AceType
        $aceSizeHex = "0x{0:X}" -f [int]$ace.BinaryLength
        $aceFlagsHex = "0x{0:X}" -f [int]$ace.AceFlags
        $accessMaskHex = "0x{0:X}" -f [int]$ace.AccessMask

        Write-Host "${spaces}Ace Sid: $($PSStyle.Foreground.Cyan)$($ace.SecurityIdentifier)$($PSStyle.Reset)"
        Write-Host "${spaces}AceType: $($PSStyle.Foreground.Cyan)$aceTypeHex$($PSStyle.Reset) ($($ace.AceType))"
        Write-Host "${spaces}AceSize: $($PSStyle.Foreground.Cyan)$aceSizeHex$($PSStyle.Reset) ($($ace.BinaryLength))"
        Write-Host "${spaces}AceFlags: $($PSStyle.Foreground.Cyan)$aceFlagsHex$($PSStyle.Reset) ($($ace.AceFlags))"
        Write-Host "${spaces}Access Mask: $($PSStyle.Foreground.Cyan)$accessMaskHex$($PSStyle.Reset) ($($ace.AccessMask))"
        Write-AccessMask $ace.AccessMask -indent ($indent + 4)
    }
}

function Write-AccessMask {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [int]$accessMask,

        [Parameter(Mandatory = $false)]
        [int]$indent = 0,

        [Parameter(Mandatory = $false)]
        [switch]$ShowFullList = $false
    )

    $spaces = " " * $indent
    $mask = [AccessMask]::new($accessMask)

    $checkmark = [System.Char]::ConvertFromUtf32([System.Convert]::ToInt32("2713", 16))
    $cross = [System.Char]::ConvertFromUtf32([System.Convert]::ToInt32("2717", 16))
    
    if ($ShowFullList) {
        Write-Host "${spaces}simplified list:"
    }
    
    # Write "basic permissions" first (e.g. composite rights like "Read", "Write", "Modify", etc.)
    $checkedValues = 0
    foreach ($key in [Enum]::GetValues([BasicPermissions])) {
        $value = $key.value__
        if ($mask.Has($value)) {
            $checkedValues = $checkedValues -bor $value
            Write-Host "${spaces}$($PSStyle.Foreground.Green)$checkmark$($PSStyle.Reset) $key"
        }
        else {
            Write-Host "${spaces}$($PSStyle.Foreground.Red)$cross$($PSStyle.Reset) $key"
        }
    }
    
    # Write if there are any permissions not covered by basic
    $remaining = [AccessMask]::new($accessMask)
    $remaining.Remove($checkedValues)
    if ($remaining.Value -ne 0) {
        # Check what known values remain, in addition to the values already checked above
        $allValues = [Enum]::GetValues([SpecificRights]) + [Enum]::GetValues([StandardRights]) + [Enum]::GetValues([GenericRights])
        $remainingValueList = $allValues | Where-Object { $remaining.Has($_.value__) }
        
        # If there are any bits not covered by the known permissions, add it to the list
        $remainingValueList | ForEach-Object { $remaining.Remove($_.value__) }
        if ($remaining.Value -ne 0) {
            $remainingValueList += [string]::Format("0x{0:X}", $remaining.Value)
        }

        $remainingString = $remainingValueList -join ", "
        Write-Host "${spaces}$($PSStyle.Foreground.Green)$checkmark$($PSStyle.Reset) SPECIAL_PERMISSIONS ($remainingString)"
    }
    else {
        Write-Host "${spaces}$($PSStyle.Foreground.Red)$cross$($PSStyle.Reset) $key SPECIAL_PERMISSIONS"
    }

    # Optionally write the full list of permissions bits
    if ($ShowFullList) {
        Write-Host "${spaces}full list:"
        foreach ($key in [Enum]::GetValues([SpecificRights]) + [Enum]::GetValues([StandardRights]) + [Enum]::GetValues([GenericRights])) {
            $value = $key.value__
            if ($mask.Has($value)) {
                Write-Host "${spaces} ${key}"
                $mask.Remove($value)
            }
        }

        if ($mask.Value -ne 0) {
            $hex = "0x{0:X}" -f $mask.Value
            Write-Host "${spaces} Others: $hex"
        }
    }
}