Helpers/SddlUtils.ps1

function Get-AceFlagsFromInheritanceAndPropagation {
    [OutputType([System.Security.AccessControl.AceFlags])]
    param (
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.InheritanceFlags]$InheritanceFlags,

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

    $aceFlags = [System.Security.AccessControl.AceFlags]::None
    if ($InheritanceFlags -band [System.Security.AccessControl.InheritanceFlags]::ContainerInherit) {
        $aceFlags = ([int]$aceFlags -bor [int][System.Security.AccessControl.AceFlags]::ContainerInherit)
    }
    if ($InheritanceFlags -band [System.Security.AccessControl.InheritanceFlags]::ObjectInherit) {
        $aceFlags = ([int]$aceFlags -bor [int][System.Security.AccessControl.AceFlags]::ObjectInherit)
    }
    if ($PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly) {
        $aceFlags = ([int]$aceFlags -bor [int][System.Security.AccessControl.AceFlags]::InheritOnly)
    }
    if ($PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::NoPropagateInherit) {
        $aceFlags = ([int]$aceFlags -bor [int][System.Security.AccessControl.AceFlags]::NoPropagateInherit)
    }
    return $aceFlags
}

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
        }
    }
}

function Get-EmptyRawAcl {
    $revision = [AclRevision]::ACL_REVISION
    $capacity = 0
    return [System.Security.AccessControl.RawAcl]::new([int]$revision, $capacity)
}

function Reset-SecurityDescriptor {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Low')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Security.AccessControl.RawSecurityDescriptor]$SecurityDescriptor
    )

    process {
        $flagsToRemove = [System.Security.AccessControl.ControlFlags]::DiscretionaryAclAutoInherited -bor `
                         [System.Security.AccessControl.ControlFlags]::DiscretionaryAclProtected -bor `
                         [System.Security.AccessControl.ControlFlags]::SystemAclAutoInherited -bor `
                         [System.Security.AccessControl.ControlFlags]::SystemAclProtected -bor `
                         [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent -bor `
                         [System.Security.AccessControl.ControlFlags]::SystemAclPresent
        
        $controlFlags = [int]$SecurityDescriptor.ControlFlags -band (-bnot [int]$flagsToRemove)

        if ($PSCmdlet.ShouldProcess("SecurityDescriptor", "Reset ControlFlags, DACL and SACL")) {
            $SecurityDescriptor.DiscretionaryAcl = Get-EmptyRawAcl
            $SecurityDescriptor.SystemAcl = Get-EmptyRawAcl
            $SecurityDescriptor.SetFlags($controlFlags)
        }
    }
}

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 {
<#
    .SYNOPSIS
    Displays a detailed, formatted view of a security descriptor including owner, group, control flags, and ACLs.
 
    .DESCRIPTION
    The Write-SecurityDescriptor function provides a comprehensive, human-readable display of a security descriptor's
    components It displays the owner, group, control flags, discretionary ACL (DACL), and system ACL (SACL) with
    color-coded formatting for enhanced readability. This function is particularly useful for debugging, auditing, and
    understanding the structure of Windows security descriptors.
 
    .PARAMETER Acl
    Specifies the security descriptor or ACL to display. This can be in various formats including SDDL (Security
    Descriptor Definition Language) string, base64-encoded binary, array of bytes, CommonSecurityDescriptor or
    RawSecurityDescriptor objects.
 
    .PARAMETER AclFormat
    Specifies the format of the input ACL. If not provided, the function will automatically infer the format.
    Supported formats include SDDL, Base64, Binary, and Raw.
 
    .OUTPUTS
    System.Void
    This function outputs formatted text to the console and does not return any objects.
 
    .EXAMPLE
    PS> $acl = "O:BAG:SYD:(A;;FA;;;SY)(A;;0x1200a9;;;BU)"
    PS> Write-SecurityDescriptor -Acl $acl -AclFormat Sddl
 
    Displays a formatted view of the SDDL security descriptor, showing owner, group, control flags, and both
    discretionary and system ACLs with detailed access mask information.
 
    .EXAMPLE
    PS> $context = Get-AzStorageContext -StorageAccountName "mystorageaccount" -StorageAccountKey "mykey"
    PS> Get-AzFileAcl -Context $context -FileShareName "myshare" -FilePath "folder/file.txt" | Write-SecurityDescriptor
 
    .LINK
    Get-AzFileAcl
 
    .LINK
    Convert-SecurityDescriptor
#>

    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object]$Acl,

        [Parameter(Mandatory = $false)]
        [SecurityDescriptorFormat]$AclFormat
    )

    process {
        if ($null -eq $AclFormat) {
            $AclFormat = Get-InferredAclFormat $Acl
            Write-Verbose "Inferred ACL format: $AclFormat. To override, use -AclFormat."
        }

        $descriptor = Convert-SecurityDescriptor $Acl -From $AclFormat -To Raw
        
        $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 {
<#
    .SYNOPSIS
    Displays a detailed, formatted view of a ACE's access mask.
 
    .DESCRIPTION
    The Write-AccessMask function provides a comprehensive, human-readable display of an ACE's access mask.
 
    .PARAMETER AccessMask
    Specifies the access mask to display.
 
    .PARAMETER ShowFullList
    If specified, the function will display the full list of individual permission bits in addition to the basic
    permissions.
 
    .OUTPUTS
    System.Void
    This function outputs formatted text to the console and does not return any objects.
 
    .EXAMPLE
    PS> Write-AccessMask 0x1200a9
#>

    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [int]$accessMask,

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

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

    # Convert generics to specifics
    $accessMask = Get-MappedAccessMask -AccessMask $accessMask

    $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) 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"
        }
    }
}