fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
<#
|
|
|
|
|
Tests Get-NTFSSecurityDescriptor and Set-NTFSSecurityDescriptor of the module built in NTFSSecurity\bin\Release on
|
|
|
|
|
files in a sandbox folder. Tests that need the Security or the Restore privilege skip without it; CI runs them
|
|
|
|
|
elevated.
|
|
|
|
|
#>
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
|
|
|
|
# Assigning an owner other than the user or one of its groups needs the Restore privilege.
|
|
|
|
|
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
|
|
|
|
|
Import-Module -Name $modulePath -Force -ErrorAction Stop
|
|
|
|
|
$sandbox = New-TestSandbox -Name 'SecurityDescriptor'
|
|
|
|
|
Push-Location -LiteralPath $sandbox
|
|
|
|
|
|
|
|
|
|
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
|
|
|
|
|
$enablePrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$sidType = [System.Security.Principal.SecurityIdentifier]
|
|
|
|
|
# An owner that the user can assign only with the Restore privilege
|
|
|
|
|
$trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
|
|
|
|
|
|
|
|
|
|
function Get-RestorePrivilegeState {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-EveryoneRule ([string] $Path) {
|
|
|
|
|
(Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, $sidType) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
Pop-Location
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSSecurityDescriptor' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet read the DACL together with the SACL when the process held the Security privilege.
|
|
|
|
|
# When the folder has no SACL, Windows then returns the inherited entries of a DACL without the auto-inherit flag,
|
|
|
|
|
# such as that of a file in the temp folder of the user, without their inherited flag.
|
|
|
|
|
It 'Should report the inherited access entries as inherited' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inherited'
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
@($sd.SecurityDescriptor.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
@($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the owner and the audit entries with the access entries' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Sections'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$sd.SecurityDescriptor.GetOwner($sidType).Value | Should -Be (Get-Acl -LiteralPath $file).GetOwner($sidType).Value
|
|
|
|
|
@($sd.SecurityDescriptor.GetAuditRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Set-NTFSSecurityDescriptor' {
|
|
|
|
|
Context 'Parameter declaration' {
|
|
|
|
|
# Its siblings (Get-NTFSSecurityDescriptor and the cmdlets with a -SecurityDescriptor parameter set) take
|
|
|
|
|
# their first positional parameter at 1; -SecurityDescriptor was the only one at 2 (6c), cosmetic only: a
|
|
|
|
|
# positional call with one argument already bound to it, because it was the only positional parameter.
|
|
|
|
|
It 'Should declare -SecurityDescriptor at position 1, and still accept it positionally' {
|
|
|
|
|
(Get-Command -Name 'Set-NTFSSecurityDescriptor').Parameters['SecurityDescriptor'].Attributes.Position | Should -Be 1
|
|
|
|
|
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'PositionalWrite'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
Context 'When a cmdlet added an access entry to the descriptor' {
|
|
|
|
|
It 'Should write the entry as the only explicit entry of the item' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddedEntry'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
@((Get-Acl -LiteralPath $file).GetAccessRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote every section that Get-NTFSSecurityDescriptor read, also the unchanged
|
|
|
|
|
# owner, which Windows refuses without the Restore privilege, like a file server that refuses the owner (#34).
|
|
|
|
|
It 'Should write an added access entry and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an added audit entry and keep the owner' -Skip:(-not ($holdsSecurityPrivilege -and $canAssignAnyOwner)) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwnerAudit'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAudit -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the descriptor has sections that it did not change' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote every section that Get-NTFSSecurityDescriptor read, so it also undid the
|
|
|
|
|
# changes that were made to the item after it was read.
|
|
|
|
|
It 'Should not write back the access entries of an unchanged descriptor' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Unchanged'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0'),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $file -AclObject $acl
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write the owner when only the owner changed' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NewOwner'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$sd.SecurityDescriptor.SetOwner((New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $trustedInstaller))
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With -Verbose' {
|
|
|
|
|
It 'Should name the sections that it writes' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'VerboseChanged'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$messages = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -Verbose 4>&1
|
|
|
|
|
|
|
|
|
|
$messages.Message | Should -Contain "Writing the changed sections of the security descriptor of '$($sd.FullName)': Access"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should say that it writes nothing for an unchanged descriptor' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'VerboseUnchanged'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$messages = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -Verbose 4>&1
|
|
|
|
|
|
|
|
|
|
$messages.Message |
|
|
|
|
|
Should -Contain "No section of the security descriptor of '$($sd.FullName)' changed since it was read or last written; nothing is written"
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Context 'When the write is denied until the cmdlet takes ownership' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc4, the cmdlet set the previous owner back after the write, which undid an owner that the
|
|
|
|
|
# descriptor set, and failed for a previous owner that the user can't assign. The deny entry for the user stops
|
|
|
|
|
# the first write; as the owner, the user may change the permissions.
|
|
|
|
|
It 'Should keep the owner that the descriptor sets when the write succeeds' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryOwner'
|
|
|
|
|
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ $currentUser = 'ChangePermissions' }
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
$sd.SecurityDescriptor.SetOwner((New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-544'))
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be 'S-1-5-32-544'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, the cmdlet wrote no object with -PassThru when it had to take ownership for the write.
|
|
|
|
|
It 'Should return the written descriptor with -PassThru also when it took ownership for the write' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryPassThru'
|
|
|
|
|
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ $currentUser = 'ChangePermissions' }
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
$sd.SecurityDescriptor.SetOwner((New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-544'))
|
|
|
|
|
|
|
|
|
|
$result = @(Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -PassThru -ErrorVariable setErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -Be $file
|
|
|
|
|
$result[0].SecurityDescriptor.GetOwner($sidType).Value | Should -Be 'S-1-5-32-544'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# With a cleared, protected DACL, nobody keeps the right to set an owner, so setting the previous owner back would
|
|
|
|
|
# fail. The user owned the item already, so there is no owner to set back.
|
|
|
|
|
It 'Should not report an owner that did not change when the write that took ownership leaves an empty DACL' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryUnchangedOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $currentUser
|
|
|
|
|
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' }
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Clear-NTFSAccess -SecurityDescriptor $sd -DisableInheritance -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.GetOwner($sidType).Value | Should -Be $currentUser
|
|
|
|
|
$acl.AreAccessRulesProtected | Should -BeTrue
|
|
|
|
|
@($acl.GetAccessRules($true, $true, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Without the Restore privilege, the user can't set an owner such as TrustedInstaller back. The cmdlet reports it
|
|
|
|
|
# after it wrote the descriptor.
|
|
|
|
|
It 'Should report RestoreOwnerError for a previous owner that it cannot set back after the write' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryRestoreDenied'
|
|
|
|
|
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ $currentUser = 'ChangePermissions' }
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -HaveCount 1
|
|
|
|
|
$setErrors[0].FullyQualifiedErrorId | Should -BeLike 'RestoreOwnerError,*'
|
|
|
|
|
$setErrors[0].CategoryInfo.Category | Should -Be 'WriteError'
|
|
|
|
|
$setErrors[0].TargetObject.FullName | Should -Be $file
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $currentUser
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The user can set a group of its access token back as the owner without the Restore privilege, such as the group
|
|
|
|
|
# Administrators of an elevated session, so the cmdlet restores the owner and reports nothing. A deny entry for
|
|
|
|
|
# OWNER RIGHTS stops the first write, also for the owner; taking ownership drops that entry.
|
|
|
|
|
It 'Should set a previous owner back that the user can assign after the write that took ownership' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$administrators = 'S-1-5-32-544'
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryRestored'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $administrators
|
|
|
|
|
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' }
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
fix: pass on what a later command raises, and return every item for -Filter *.*
A throw in a later command, or an error with -ErrorAction Stop, reaches a cmdlet through its Write call as an ordinary exception. The catch for the failures of an item reported it as the error of that item and went on, so that Remove-Item2 -PassThru removed the next item after a throw, and the caller never saw the exception. The earlier check found only the end of the pipeline and a break or continue. BaseCmdlet now notes the exception that its WriteObject, WriteVerbose, and WriteDebug raised, and every catch that can enclose a write passes it on; Get-DiskSpace writes outside its try. Set-NTFSSecurityDescriptor and Get-FileHash2 also caught it at a verbose message.
Get-ChildItem2 -Filter *.* returns every item, as Get-ChildItem does. The cmdlet compared each name with the pattern again and dropped the items without a dot, most folders among them; the dot stays an ordinary character in other patterns.
The failed lookup of InheritedFrom frees its native buffer. The help paragraph of -Filter has no pair of asterisks, which platyPS turns into emphasis, and the page has an example for *.*.
Review of the independent pass: the restored-owner test asserts that a plain write is denied, the drive-mapping helper has guard tests and takes letters that the no-volume tests do not, and the pipeline tests cover a throw, an error with -ErrorAction Stop, and the verbose and debug streams for every cmdlet that can reach the code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
# A plain write of the DACL is denied, so that the cmdlet has to take ownership for its write.
|
|
|
|
|
{ Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-5-32-546' = 'ReadData' } } | Should -Throw
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable setErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -BeNullOrEmpty
|
|
|
|
|
@(Get-EveryoneRule -Path $file) | Should -HaveCount 1
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $administrators
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'A descriptor that cannot be written' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$savedPrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterEach {
|
|
|
|
|
$privateData['EnablePrivileges'] = $savedPrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report a denied write, return no failed item, and write the next descriptor' {
|
|
|
|
|
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorWriteDenied'
|
|
|
|
|
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorWriteNext'
|
|
|
|
|
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
|
|
|
|
|
$before = (Get-Acl -LiteralPath $blocked).Sddl
|
|
|
|
|
$descriptors = @(Get-NTFSSecurityDescriptor -Path $blocked, $next)
|
|
|
|
|
$descriptors | Should -HaveCount 2
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $descriptors -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$result = @($descriptors | Set-NTFSSecurityDescriptor -PassThru -ErrorVariable setErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -HaveCount 1
|
|
|
|
|
$setErrors[0].FullyQualifiedErrorId | Should -BeLike 'WriteSdError,*'
|
|
|
|
|
$setErrors[0].CategoryInfo.Category | Should -Be 'WriteError'
|
|
|
|
|
$setErrors[0].TargetObject.FullName | Should -Be $blocked
|
|
|
|
|
(Get-Acl -LiteralPath $blocked).Sddl | Should -BeExactly $before
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -Be $next
|
|
|
|
|
@(Get-EveryoneRule -Path $next) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report a deleted target and still write the next descriptor' {
|
|
|
|
|
$deleted = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorDeleted'
|
|
|
|
|
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorAfterDeleted'
|
|
|
|
|
$descriptors = @(Get-NTFSSecurityDescriptor -Path $deleted, $next)
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $descriptors -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $deleted
|
|
|
|
|
Remove-Item -LiteralPath $deleted
|
|
|
|
|
|
|
|
|
|
$result = @(Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptors -PassThru -ErrorVariable setErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$setErrors | Should -HaveCount 1
|
|
|
|
|
$setErrors[0].FullyQualifiedErrorId | Should -BeLike 'WriteSdError,*'
|
|
|
|
|
$setErrors[0].TargetObject.FullName | Should -Be $deleted
|
|
|
|
|
$deleted | Should -Not -Exist
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -Be $next
|
|
|
|
|
@(Get-EveryoneRule -Path $next) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the written descriptor denies reading it again' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, the cmdlet read the item again for -PassThru inside the block that retries a denied write,
|
|
|
|
|
# so that a denied read started an ownership retry and ended in a WriteSdError, although the write succeeded.
|
|
|
|
|
It 'Should write the descriptor and report a read error for -PassThru, not a write error' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThruDenied'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
# A deny entry for OWNER RIGHTS replaces the right of the owner to read the security descriptor.
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-3-4' -AccessRights ReadPermissions -AccessType Deny -AppliesTo ThisFolderOnly
|
|
|
|
|
|
|
|
|
|
$result = @(Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -PassThru -ErrorVariable setErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$setErrors | Should -HaveCount 1
|
|
|
|
|
$setErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
# Get-Acl of an elevated Windows PowerShell still reads the item, so .NET checks that the entry was written;
|
|
|
|
|
# .NET Core has the method as an extension method.
|
|
|
|
|
$info = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $file
|
|
|
|
|
$denied = $null
|
|
|
|
|
try {
|
|
|
|
|
if ($PSVersionTable.PSEdition -eq 'Desktop') {
|
|
|
|
|
$null = $info.GetAccessControl()
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$null = [System.IO.FileSystemAclExtensions]::GetAccessControl($info)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
$denied = $_.Exception.GetBaseException()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$denied | Should -BeOfType [System.UnauthorizedAccessException]
|
|
|
|
|
}
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'FileSystemSecurity2.Write with another item' {
|
|
|
|
|
# Before 5.0.0-rc4, Write wrote every section that the descriptor held to the other item, also the owner that
|
|
|
|
|
# Windows returns with a DACL without the auto-inherit flag, which fails for an owner that the user can't assign.
|
|
|
|
|
It 'Should write only the sections that were read, given the item as <_>' -Skip:(-not $canAssignAnyOwner) -ForEach @(
|
|
|
|
|
'FileSystemInfo', 'String'
|
|
|
|
|
) {
|
|
|
|
|
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'Source'
|
|
|
|
|
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'Target'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $source -Sid $trustedInstaller
|
|
|
|
|
$targetOwner = (Get-Acl -LiteralPath $target).GetOwner($sidType).Value
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $source), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
$sd.SecurityDescriptor.GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $target
|
|
|
|
|
$destination = if ($_ -eq 'String') { $target } else { Get-Item2 -Path $target }
|
|
|
|
|
|
|
|
|
|
{ $sd.Write($destination) } | Should -Not -Throw
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $target).GetOwner($sidType).Value | Should -Be $targetOwner
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
}
|
fix: compare output objects and report InheritedFrom correctly
Equals of FileSystemAccessRule2, FileSystemAuditRule2, and
FileSystemSecurity2 cast its argument to the .NET type, which throws for
the wrapper types themselves: -eq and -contains, and in PowerShell 7 also
Select-Object -Unique and Compare-Object, stopped with an
InvalidCastException. GetHashCode of FileSystemSecurity2 read a field
that is never set and threw a NullReferenceException. Two objects are now
equal when they hold the same entry or descriptor, like the .NET types.
InheritedFrom: Win32.GetInheritedFrom returned the sources of the SACL
whenever the descriptor had one, also for the access entries, and the
callers gave the filtered entries of -ExcludeExplicit the sources of the
first entries of the ACL. Get-NTFSAccess -SecurityDescriptor stopped with
an ArgumentOutOfRangeException for a descriptor with audit entries, as
Get-NTFSSecurityDescriptor reads them in an elevated session. The method
now takes the ACL of the entries, and the callers map the sources before
they filter.
The coverage report of rc6 pointed at both; each test fails without its
fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, comparing a descriptor threw an InvalidCastException, and its hash code a NullReferenceException,
|
|
|
|
|
# so -eq and a hashtable with the descriptor as key failed.
|
|
|
|
|
Describe 'Comparing security descriptors' {
|
|
|
|
|
It 'Should find a descriptor equal to itself and not to another one, and use it as a key' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Compare'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$other = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$sd -eq $sd | Should -BeTrue
|
|
|
|
|
$sd -eq $other | Should -BeFalse
|
|
|
|
|
$sd.Equals('x') | Should -BeFalse
|
|
|
|
|
$table = @{}
|
|
|
|
|
$table[$sd] = 'first'
|
|
|
|
|
$table[$other] = 'second'
|
|
|
|
|
$table[$sd] | Should -Be 'first'
|
|
|
|
|
$table.Count | Should -Be 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, the conversion returned a field that was never set, so it gave $null.
|
|
|
|
|
It 'Should convert to the security object of .NET that it holds' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertFile'
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertFolder' -Directory
|
|
|
|
|
$fileSd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$folderSd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
[object]::ReferenceEquals([System.Security.AccessControl.FileSecurity] $fileSd, $fileSd.SecurityDescriptor) | Should -BeTrue
|
|
|
|
|
[object]::ReferenceEquals([System.Security.AccessControl.DirectorySecurity] $folderSd, $folderSd.SecurityDescriptor) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should be equal only to a descriptor of the module, in both directions' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Symmetric'
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
$raw = $sd.SecurityDescriptor
|
|
|
|
|
|
|
|
|
|
$sd.Equals($raw) | Should -BeFalse
|
|
|
|
|
$raw.Equals($sd) | Should -BeFalse
|
|
|
|
|
}
|
fix: compare output objects and report InheritedFrom correctly
Equals of FileSystemAccessRule2, FileSystemAuditRule2, and
FileSystemSecurity2 cast its argument to the .NET type, which throws for
the wrapper types themselves: -eq and -contains, and in PowerShell 7 also
Select-Object -Unique and Compare-Object, stopped with an
InvalidCastException. GetHashCode of FileSystemSecurity2 read a field
that is never set and threw a NullReferenceException. Two objects are now
equal when they hold the same entry or descriptor, like the .NET types.
InheritedFrom: Win32.GetInheritedFrom returned the sources of the SACL
whenever the descriptor had one, also for the access entries, and the
callers gave the filtered entries of -ExcludeExplicit the sources of the
first entries of the ACL. Get-NTFSAccess -SecurityDescriptor stopped with
an ArgumentOutOfRangeException for a descriptor with audit entries, as
Get-NTFSSecurityDescriptor reads them in an elevated session. The method
now takes the ACL of the entries, and the callers map the sources before
they filter.
The coverage report of rc6 pointed at both; each test fails without its
fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
}
|