SddlUtils.ps1
function ConvertTo-RawSecurityDescriptor { param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$sddl ) # There are multiple ways to parse SDDL, but RawSecurityDescriptor is the most complete. # # ConvertFrom-SddlString builds a SecurityDescriptorInfo, where the raw view is a CommonSecurityDescriptor (which is a subclass of RawSecurityDescriptor). # However, CommonSecurityDescriptor drops the inheritance and propagation flags, which are important for our use case. # (see https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ConvertFrom-SddlString.cs) # # The .NET SDK has System.Security.AccessControl.DirectorySecurity and System.Security.AccessControl.FileSecurity. On both of these, # we can parse SDDL with the SetSecurityDescriptorSddlForm method. DirectorySecurity keeps the inheritance and propagation flags, and FileSecurity does not. # This seems like a good candidate but there is a bug on PowerShell 7 where this method doesn't work. # (see https://github.com/PowerShell/PowerShell/issues/19094) return [System.Security.AccessControl.RawSecurityDescriptor]::new($sddl) } function ConvertFrom-RawSecurityDescriptor { param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [System.Security.AccessControl.RawSecurityDescriptor]$descriptor ) return $descriptor.GetSddlForm([System.Security.AccessControl.AccessControlSections]::All) } 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 ) foreach ($ace in $SecurityDescriptor.DiscretionaryAcl) { $hasAllBitsFromEnabledFlags = ($ace.AceFlags -band $EnabledFlags) -eq $EnabledFlags $hasNoBitsFromDisabledFlags = ($ace.AceFlags -band $DisabledFlags) -eq 0 if (-not ($hasAllBitsFromEnabledFlags -and $hasNoBitsFromDisabledFlags)) { return $false } } return $true } function Set-AceFlags { 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 ) if ($EnableFlags -band $DisableFlags) { throw "Enable and disable flags cannot overlap" } # Create new ACEs with updated flags $newAces = $SecurityDescriptor.DiscretionaryAcl | ForEach-Object { $aceFlags = ($_.AceFlags -bor $EnableFlags) -band (-bnot $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 } } |