|
|
|
|
<#
|
|
|
|
|
Tests the access cmdlets of the module built in NTFSSecurity\bin\Release on files in a sandbox folder.
|
|
|
|
|
#>
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
# With the Restore privilege, Windows may grant writing the DACL despite a deny entry.
|
|
|
|
|
$canBypassWriteDeny = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
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
|
|
|
# Assigning an owner other than the user or one of its groups needs the Restore privilege.
|
|
|
|
|
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
|
|
|
|
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
fix(access): don't warn for a name of this computer in Get-NTFSEffectiveAccess
Where a computer doesn't offer the remote interface of the authorization
manager, the cmdlet calculates the result with the local one and warns
that the result might be inaccurate. Only localhost in lowercase counted
as this computer, so the cmdlet warned that the computer couldn't be
reached for ., LOCALHOST, or the computer name, although the local result
is the result of that computer. A name of this computer is now localhost
in any case, ., the NetBIOS name, the DNS host name, or the fully
qualified domain name.
From the security-reviewer pass over be04cb7..4ee01e5 (Nit 7),
reproduced in Windows PowerShell 5.1 and PowerShell 7 on a workstation
without the remote interface.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
# Names of this computer for -ServerName of Get-NTFSEffectiveAccess: its NetBIOS name, its DNS host name, and, in a
|
|
|
|
|
# domain, its fully qualified name
|
|
|
|
|
$ipProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
|
|
|
|
|
$localComputerNames = @(@('LOCALHOST', '.', $env:COMPUTERNAME, $ipProperties.HostName) +
|
|
|
|
|
@(if ($ipProperties.DomainName) { '{0}.{1}' -f $ipProperties.HostName, $ipProperties.DomainName }) | Sort-Object -Unique)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 'Access'
|
|
|
|
|
Push-Location -LiteralPath $sandbox
|
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
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
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
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
Pop-Location
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSAccess' {
|
|
|
|
|
Context 'When a path fails after a readable path' {
|
|
|
|
|
# Before 5.0.0, the cmdlet wrote the entries of the previous item again for the failing path.
|
|
|
|
|
It 'Should return the entries of the first item once' {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Readable' -Directory
|
|
|
|
|
$denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied'
|
|
|
|
|
Block-TestReadPermission -Sandbox $sandbox -Path $denied
|
|
|
|
|
$expected = @(Get-NTFSAccess -Path $folder).Count
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAccess -Path $folder, $denied -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
@($entries | Where-Object -Property FullName -EQ -Value $folder) | Should -HaveCount $expected
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Describe 'Get-NTFSEffectiveAccess' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$effectiveFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Effective'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $effectiveFile
|
|
|
|
|
$acl = Get-Acl -LiteralPath $effectiveFile
|
|
|
|
|
$guests = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-546'
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$guests, [System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Deny
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $effectiveFile -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the warning misspelled the privilege as "Privliege".
|
|
|
|
|
It 'Should warn once that the Security privilege is missing' -Skip:$holdsSecurityPrivilege {
|
|
|
|
|
Get-NTFSEffectiveAccess -Path $effectiveFile -WarningVariable accessWarnings -WarningAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
|
|
$accessWarnings | Should -HaveCount 1
|
|
|
|
|
$accessWarnings[0].Message | Should -BeExactly 'The user does not hold the Security privilege and might not be able to read the effective permissions.'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should leave out an account without access when -ExcludeNoneAccessEntries is used' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -Account 'S-1-5-32-546' -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return an account with access when -ExcludeNoneAccessEntries is used' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should use the current location when -Path is omitted' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -BeLike ('*\{0}' -f (Split-Path -Path $sandbox -Leaf))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compute the effective access of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $effectiveFile
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -Be $effectiveFile
|
|
|
|
|
}
|
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 effective access cannot be calculated' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc4, the cmdlet blamed a missing Security privilege for every failure while the privilege
|
|
|
|
|
# wasn't enabled (#109). A security descriptor without an owner is such a failure: the DACL of the file has the
|
|
|
|
|
# auto-inherit flag since Set-Acl wrote it, so Windows returns no owner when only the DACL is read.
|
|
|
|
|
It 'Should name the cause in the error, not the Security privilege' {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Security').PrivilegeState | Should -Not -Be 'Enabled'
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $effectiveFile), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
$sd.SecurityDescriptor.GetOwner($sidType) | Should -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
Get-NTFSEffectiveAccess -SecurityDescriptor $sd -ErrorVariable accessErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -HaveCount 1
|
|
|
|
|
$accessErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetEffectiveAccessError,*'
|
|
|
|
|
$accessErrors[0].Exception.Message | Should -Not -BeLike '*Enable-Privileges*'
|
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 computer of -ServerName cannot be reached' {
|
|
|
|
|
# Before 5.0.0-rc5, the cmdlet returned no access when the computer couldn't be reached, although it warned that
|
|
|
|
|
# it had calculated the result on this computer. Windows reports a computer that it can't resolve or reach with
|
|
|
|
|
# the error RPC server unavailable; the name ends in .invalid, which no DNS server resolves (RFC 2606).
|
|
|
|
|
It 'Should return the result of this computer and warn' {
|
|
|
|
|
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningAction SilentlyContinue -ErrorAction Stop
|
|
|
|
|
[long] $expected.AccessRights | Should -BeGreaterThan ([long] [Security2.FileSystemRights2]::Synchronize)
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName 'ntfssecurity-test.invalid' -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AccessRights | Should -Be $expected.AccessRights
|
|
|
|
|
# Before 5.0.0-rc7, the warning didn't name the computer.
|
|
|
|
|
$accessWarnings.Message | Should -Contain ("The effective rights can only be computed based on group membership on this computer, " +
|
|
|
|
|
"because the computer 'ntfssecurity-test.invalid' can't be reached for a remote access check. " +
|
|
|
|
|
'For more accurate results, calculate effective access rights on that computer.')
|
|
|
|
|
}
|
test: cover token handles, drive-root writes, name filters, and descriptor APIs
New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.
Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# An empty name names no computer, so it names this one no more than any other name that can't be reached.
|
|
|
|
|
It 'Should return the result of this computer and warn for an empty -ServerName' {
|
|
|
|
|
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningAction SilentlyContinue -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName '' -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AccessRights | Should -Be $expected.AccessRights
|
|
|
|
|
$accessWarnings.Message | Should -Contain ("The effective rights can only be computed based on group membership on this computer, " +
|
|
|
|
|
"because the computer '' can't be reached for a remote access check. " +
|
|
|
|
|
'For more accurate results, calculate effective access rights on that computer.')
|
|
|
|
|
}
|
|
|
|
|
}
|
fix(access): don't warn for a name of this computer in Get-NTFSEffectiveAccess
Where a computer doesn't offer the remote interface of the authorization
manager, the cmdlet calculates the result with the local one and warns
that the result might be inaccurate. Only localhost in lowercase counted
as this computer, so the cmdlet warned that the computer couldn't be
reached for ., LOCALHOST, or the computer name, although the local result
is the result of that computer. A name of this computer is now localhost
in any case, ., the NetBIOS name, the DNS host name, or the fully
qualified domain name.
From the security-reviewer pass over be04cb7..4ee01e5 (Nit 7),
reproduced in Windows PowerShell 5.1 and PowerShell 7 on a workstation
without the remote interface.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# Not every computer offers the remote interface of the authorization manager; the cmdlet then calculates the result
|
|
|
|
|
# with the local one, which for a name of this computer is the result of that computer. Before 5.0.0-rc7, the cmdlet
|
|
|
|
|
# warned that the computer couldn't be reached for every name of this computer but localhost in lowercase.
|
|
|
|
|
Context 'When -ServerName names this computer' {
|
|
|
|
|
It 'Should return the result of localhost for <_> and warn no more than for localhost' -ForEach $localComputerNames {
|
|
|
|
|
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningVariable expectedWarnings -WarningAction SilentlyContinue -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName $_ -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AccessRights | Should -Be $expected.AccessRights
|
|
|
|
|
# Without the Security privilege, the cmdlet warns about it for every name.
|
|
|
|
|
@($accessWarnings.Message) -join '|' | Should -Be (@($expectedWarnings.Message) -join '|')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSOrphanedAccess' {
|
|
|
|
|
# Before 5.0.0, a path with braces stopped the cmdlet with a FormatException (#3), which the verbose message raised.
|
|
|
|
|
It 'Should read a folder whose name contains braces' {
|
|
|
|
|
$braces = Join-Path -Path $sandbox -ChildPath ('{{Braces}}-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $braces
|
|
|
|
|
[IO.Directory]::CreateDirectory($braces) | Out-Null
|
|
|
|
|
|
|
|
|
|
$messages = @(Get-NTFSOrphanedAccess -Path $braces -Verbose -ErrorAction Stop 4>&1)
|
|
|
|
|
|
|
|
|
|
$messages.Message | Should -Contain "Item $braces knows about 0 orphaned SIDs in its ACL"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Orphaned'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $orphanedFile
|
|
|
|
|
$acl = Get-Acl -LiteralPath $orphanedFile
|
|
|
|
|
foreach ($sid in 'S-1-5-21-1-2-3-1001', 'S-1-5-21-1-2-3-1002') {
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $sid),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
Set-Acl -LiteralPath $orphanedFile -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return the entries whose account cannot be resolved' {
|
|
|
|
|
@(Get-NTFSOrphanedAccess -Path $orphanedFile) | Should -HaveCount 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the entries of -Account' {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAccess -Path $orphanedFile -Account 'S-1-5-21-1-2-3-1002')
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-1002'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the entries of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $orphanedFile
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSOrphanedAccess -SecurityDescriptor $sd)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 2
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSSimpleAccess' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$simpleFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'Simple' -Directory
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $simpleFolder
|
|
|
|
|
$acl = Get-Acl -LiteralPath $simpleFolder
|
|
|
|
|
$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 $simpleFolder -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the entries of -Account' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $simpleFolder -Account 'S-1-1-0' -IncludeRootFolder:$false)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.Identity.Sid | Should -Be 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the entries of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $simpleFolder
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $simpleFolder }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should show the entries as a table with the account, the rights, and the type' {
|
|
|
|
|
$text = Get-NTFSSimpleAccess -Path $simpleFolder -IncludeRootFolder:$false | Out-String -Width 200
|
|
|
|
|
|
|
|
|
|
$text | Should -Match 'Account\s+Access Rights\s+Type'
|
|
|
|
|
}
|
fix: report ReadData and the parent of a relative path in Get-NTFSSimpleAccess
Two deviations from the cmdlet page, found by new tests from its
contract:
- An entry that grants only ReadData became None: the reduction tested
the combined Read mask, and nothing mapped ReadData alone. .NET adds
Synchronize to every allow entry, which hid it; other tools write such
entries.
- With -IncludeRootFolder, on by default, a relative path with a single
folder name had no parent folder in the result, because the parent was
taken from the path before it was resolved.
The tests also cover the rights reduction, the comparison of folders
with their parent, the pipeline, the current location, -ExcludeExplicit,
files, and missing paths, elevated and as a basic user in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
Context 'When it reduces the rights of an entry' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
# One explicit entry per case, for a SID of its own and with exactly these rights. .NET would add
|
|
|
|
|
# Synchronize to an allow entry, so the DACL is set in SDDL.
|
|
|
|
|
$rightsFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleRights' -Directory
|
|
|
|
|
$rightsCases = @(
|
|
|
|
|
@{ Name = 'ReadData'; Mask = 0x1; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadAttributes'; Mask = 0x80; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'Traverse'; Mask = 0x20; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadPermissions'; Mask = 0x20000; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'CreateFiles'; Mask = 0x2; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'AppendData'; Mask = 0x4; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'WriteAttributes'; Mask = 0x100; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'ChangePermissions'; Mask = 0x40000; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'TakeOwnership'; Mask = 0x80000; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'Delete'; Mask = 0x10000; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'DeleteSubdirectoriesAndFiles'; Mask = 0x40; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'Modify'; Mask = 0x1301BF; Expected = 'Read, Write, Delete' }
|
|
|
|
|
@{ Name = 'FullControl'; Mask = 0x1F01FF; Expected = 'Read, Write, Delete' }
|
|
|
|
|
)
|
|
|
|
|
$rightsSid = @{}
|
|
|
|
|
$entries = for ($i = 0; $i -lt $rightsCases.Count; $i++) {
|
|
|
|
|
$sid = 'S-1-5-21-1-2-3-{0}' -f (3001 + $i)
|
|
|
|
|
$rightsSid[$rightsCases[$i].Name] = $sid
|
|
|
|
|
'(A;;0x{0:X};;;{1})' -f $rightsCases[$i].Mask, $sid
|
|
|
|
|
}
|
|
|
|
|
$acl = Get-Acl -LiteralPath $rightsFolder
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm(('D:{0}' -f ($entries -join '')), 'Access')
|
|
|
|
|
Set-Acl -LiteralPath $rightsFolder -AclObject $acl
|
|
|
|
|
|
|
|
|
|
$rightsResult = @(Get-NTFSSimpleAccess -Path $rightsFolder -ExcludeInherited -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, an entry with ReadData alone, which .NET never creates but other tools do, became None.
|
|
|
|
|
It 'Should reduce <Name> to <Expected>' -ForEach @(
|
|
|
|
|
@{ Name = 'ReadData'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadAttributes'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'Traverse'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadPermissions'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'CreateFiles'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'AppendData'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'WriteAttributes'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'ChangePermissions'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'TakeOwnership'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'Delete'; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'DeleteSubdirectoriesAndFiles'; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'Modify'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
@{ Name = 'FullControl'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
) {
|
|
|
|
|
$entry = @($rightsResult | Where-Object -FilterScript { $_.Identity.Sid -eq $rightsSid[$Name] })
|
|
|
|
|
|
|
|
|
|
$entry | Should -HaveCount 1
|
|
|
|
|
$entry[0].AccessRights.ToString() | Should -Be $Expected
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When it compares folders with their parent folder' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
# The parent grants Everyone ReadData to its subfolders and the user Full Control, so that a basic user
|
|
|
|
|
# can create the items; the child adds an entry of its own. The child is created after the DACL of the
|
|
|
|
|
# parent, so that it inherits only these entries.
|
|
|
|
|
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleParent' -Directory
|
|
|
|
|
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
|
|
|
$acl = Get-Acl -LiteralPath $parent
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm("D:P(A;OICI;0x120089;;;WD)(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;FA;;;$user)", 'Access')
|
|
|
|
|
Set-Acl -LiteralPath $parent -AclObject $acl
|
|
|
|
|
$child = Join-Path -Path $parent -ChildPath 'Child'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
|
|
|
|
|
New-Item -ItemType Directory -Path $child | Out-Null
|
|
|
|
|
$acl = Get-Acl -LiteralPath $child
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-21-1-2-3-3101'),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::Modify, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $child -AclObject $acl
|
|
|
|
|
$childFile = Join-Path -Path $child -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $childFile
|
|
|
|
|
Set-Content -LiteralPath $childFile -Value 'File' -ErrorAction Stop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report all entries of the first folder and of the following folder only what the parent does not cover' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $parent, $child -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $parent) | Should -HaveCount 4
|
|
|
|
|
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
|
|
|
|
|
$childEntries | Should -HaveCount 1
|
|
|
|
|
$childEntries[0].Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare the folders from the pipeline in the same way' {
|
|
|
|
|
$result = @(Get-Item2 -Path $parent, $child | Get-NTFSSimpleAccess -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc7, a folder whose parent folder the cmdlet hadn't reported was left out of the result, so the
|
|
|
|
|
# entries of the parent here were missing.
|
|
|
|
|
It 'Should report all entries of a folder whose parent folder it did not report' {
|
|
|
|
|
$childAlone = @(Get-NTFSSimpleAccess -Path $child -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
$parentAlone = @(Get-NTFSSimpleAccess -Path $parent -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
$parentAlone | Should -Not -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child, $parent -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child) | Should -HaveCount $childAlone.Count
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $parent) | Should -HaveCount $parentAlone.Count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc7, a folder that came after its parent folder a second time failed with a ReadError, "An item
|
|
|
|
|
# with the same key has already been added."
|
|
|
|
|
It 'Should compare a folder that it gets twice with its parent folder both times' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $parent, $child, $child -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -BeNullOrEmpty
|
|
|
|
|
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
|
|
|
|
|
$childEntries | Should -HaveCount 2
|
|
|
|
|
$childEntries | ForEach-Object -Process { $_.Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc7, a drive root after the first path was left out as well, because it has no parent folder;
|
|
|
|
|
# after another folder whose parent was reported, it was compared with that unrelated parent. The test reads the
|
|
|
|
|
# entries of the drive root and changes nothing there.
|
|
|
|
|
It 'Should report all entries of a drive root, which has no parent folder' {
|
|
|
|
|
$root = [IO.Path]::GetPathRoot($child)
|
|
|
|
|
$rootAlone = @(Get-NTFSSimpleAccess -Path $root -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
$rootAlone | Should -Not -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child, $root -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -BeNullOrEmpty
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $root) | Should -HaveCount $rootAlone.Count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# With -IncludeRootFolder, the default, the cmdlet reports the parent folder of the first path first. A drive root
|
|
|
|
|
# has none, so it reports the root itself, once. The test reads the entries of the drive root only.
|
|
|
|
|
It 'Should report no parent folder in front of a drive root by default' {
|
|
|
|
|
$root = [IO.Path]::GetPathRoot($child)
|
|
|
|
|
$rootAlone = @(Get-NTFSSimpleAccess -Path $root -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $root -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount $rootAlone.Count
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $root }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Windows doesn't distinguish paths by case. Before 5.0.0-rc7, the cmdlet didn't recognize the parent folder of a
|
|
|
|
|
# folder whose path differed from it in case, and left the folder out.
|
|
|
|
|
It 'Should compare a folder with its parent folder also when their paths differ in case' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $parent, $child.ToUpperInvariant() -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
|
|
|
|
|
$childEntries | Should -HaveCount 1
|
|
|
|
|
$childEntries[0].Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
fix: report ReadData and the parent of a relative path in Get-NTFSSimpleAccess
Two deviations from the cmdlet page, found by new tests from its
contract:
- An entry that grants only ReadData became None: the reduction tested
the combined Read mask, and nothing mapped ReadData alone. .NET adds
Synchronize to every allow entry, which hid it; other tools write such
entries.
- With -IncludeRootFolder, on by default, a relative path with a single
folder name had no parent folder in the result, because the parent was
taken from the path before it was resolved.
The tests also cover the rights reduction, the comparison of folders
with their parent, the pipeline, the current location, -ExcludeExplicit,
files, and missing paths, elevated and as a basic user in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
It 'Should report the parent folder of the first path first by default' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result[0].FullName | Should -Be $parent
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, a relative path with a single folder name had no parent folder in the result.
|
|
|
|
|
It 'Should report the parent folder of a relative path first by default' {
|
|
|
|
|
Push-Location -LiteralPath $parent
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path 'Child' -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
Pop-Location
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result[0].FullName | Should -Be $parent
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should use the current location without -Path' {
|
|
|
|
|
Push-Location -LiteralPath $child
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
Pop-Location
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $child }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the inherited entries with -ExcludeExplicit' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child -ExcludeExplicit -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 4
|
|
|
|
|
$result.Identity.Sid | Should -Not -Contain 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should skip a file without an error' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $childFile -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report the security descriptor of a file' {
|
|
|
|
|
$result = @(Get-NTFSSecurityDescriptor -Path $childFile | Get-NTFSSimpleAccess -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $childFile }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an error for a path that does not exist and continue with the next path' {
|
|
|
|
|
$missing = Join-Path -Path $parent -ChildPath 'Missing'
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $missing, $child -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -HaveCount 1
|
|
|
|
|
$simpleErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $child }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Remove-NTFSAccess' {
|
|
|
|
|
Context 'When a path does not exist' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$missing = Join-Path -Path $sandbox -ChildPath 'Missing.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the cmdlet went on with the missing item and wrote a second, misleading RemoveAceError.
|
|
|
|
|
It 'Should write only the read error' {
|
|
|
|
|
Remove-NTFSAccess -Path $missing -Account 'Everyone' -AccessRights ReadData -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -HaveCount 1
|
|
|
|
|
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should not stop with -PassThru' {
|
|
|
|
|
{ Remove-NTFSAccess -Path $missing -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorAction SilentlyContinue } |
|
|
|
|
|
Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the entries cannot be changed' {
|
|
|
|
|
# Before 5.0.0, -PassThru returned the unchanged entries of the item after the error.
|
|
|
|
|
It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny {
|
|
|
|
|
$protected = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveProtected'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $protected
|
|
|
|
|
Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
Block-TestWritePermission -Sandbox $sandbox -Path $protected
|
|
|
|
|
|
|
|
|
|
$result = @(Remove-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -HaveCount 1
|
|
|
|
|
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 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 read only the DACL, but wrote the owner that Windows returns with a DACL without
|
|
|
|
|
# the auto-inherit flag, which Windows refuses without the Restore privilege (#34). Any write of the DACL adds
|
|
|
|
|
# the flag, so the item keeps its DACL as created and has no explicit entry to remove.
|
|
|
|
|
It 'Should write no error and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveOtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With a generic right' {
|
|
|
|
|
# Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is
|
|
|
|
|
# not valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in
|
|
|
|
|
# inherit-only entries of folders.
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$guests = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-546'
|
|
|
|
|
|
|
|
|
|
function New-GenericRightFolder {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the sandbox.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Entry)
|
|
|
|
|
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder
|
|
|
|
|
$acl = Get-Acl -LiteralPath $folder
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?<flags>[A-Z]*)', ('D:${flags}' + $Entry)))
|
|
|
|
|
Set-Acl -LiteralPath $folder -AclObject $acl
|
|
|
|
|
$folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-GuestsRule {
|
|
|
|
|
param ([string] $Path)
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -Property IdentityReference -EQ -Value $guests
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an inherit-only entry that Get-NTFSAccess returned, and nothing else' -ForEach @(
|
|
|
|
|
@{ Case = 'Allow'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $false }
|
|
|
|
|
@{ Case = 'Allow with -RemoveSpecific'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $true }
|
|
|
|
|
@{ Case = 'Deny'; Entry = '(D;OICIIO;GA;;;BG)'; Specific = $false }
|
|
|
|
|
) {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry $Entry
|
|
|
|
|
$before = (Get-Acl -LiteralPath $folder).Sddl
|
|
|
|
|
$before | Should -Match ([regex]::Escape($Entry))
|
|
|
|
|
|
|
|
|
|
Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited |
|
|
|
|
|
Remove-NTFSAccess -RemoveSpecific:$Specific -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before.Replace($Entry, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove only the requested generic right from an entry with two' {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x90000000;;;BG)'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericRead -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
$rule = Get-GuestsRule -Path $folder
|
|
|
|
|
$rule | Should -HaveCount 1
|
|
|
|
|
[int] $rule.FileSystemRights | Should -Be 0x10000000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A rule that matches the entry exactly is removed as it is, with the Synchronize right that the module adds
|
|
|
|
|
# to an Allow rule; otherwise an entry with only Synchronize would be left behind.
|
|
|
|
|
It 'Should remove an entry with GenericAll and Synchronize when -AccessRights names GenericAll' {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BG)'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
Get-GuestsRule -Path $folder | Should -BeNullOrEmpty
|
|
|
|
|
}
|
test: cover token handles, drive-root writes, name filters, and descriptor APIs
New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.
Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# The entry of another account with exactly the rights to remove is not an exact match for the entry of the
|
|
|
|
|
# account, so the rights that the entry of the account keeps still have their Synchronize.
|
|
|
|
|
It 'Should take only the requested generic right from the entry of the account when another account has an exact entry' {
|
|
|
|
|
$users = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-545'
|
|
|
|
|
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BU)(A;OICIIO;0x90100000;;;BG)'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
$guestsRule = @(Get-GuestsRule -Path $folder)
|
|
|
|
|
$guestsRule | Should -HaveCount 1
|
|
|
|
|
[int] $guestsRule[0].FileSystemRights | Should -Be 0x80100000
|
|
|
|
|
$usersRule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -Property IdentityReference -EQ -Value $users)
|
|
|
|
|
$usersRule | Should -HaveCount 1
|
|
|
|
|
[int] $usersRule[0].FileSystemRights | Should -Be 0x10100000
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Context 'With -RemoveSpecific' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $removeFolder
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
function Get-EveryoneRule {
|
|
|
|
|
$sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should keep an entry that does not match exactly' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
(Get-EveryoneRule).FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Modify) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an entry that matches exactly' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
Get-EveryoneRule | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should take the rights away from a matching entry without -RemoveSpecific' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$rule = Get-EveryoneRule
|
|
|
|
|
$rule | Should -Not -BeNullOrEmpty
|
|
|
|
|
$rule.FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should keep an entry that does not match exactly, given the path' {
|
|
|
|
|
Add-NTFSAccess -Path $removeFolder -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $removeFolder -Account 'Everyone' -AccessRights ReadData -RemoveSpecific -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$rules = @((Get-Acl -LiteralPath $removeFolder).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' })
|
|
|
|
|
$rules | Should -HaveCount 1
|
|
|
|
|
$rules[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Modify) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an entry that matches exactly, given the path' {
|
|
|
|
|
Add-NTFSAccess -Path $removeFolder -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $removeFolder -Account 'Everyone' -AccessRights Modify -RemoveSpecific -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $removeFolder).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' } | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Add-NTFSAccess' {
|
|
|
|
|
Context 'When the entries cannot be changed' {
|
|
|
|
|
# Before 5.0.0, -PassThru returned the unchanged entries of the item after the error.
|
|
|
|
|
It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny {
|
|
|
|
|
$protected = New-TestSandboxItem -Sandbox $sandbox -Name 'AddProtected'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $protected
|
|
|
|
|
Block-TestWritePermission -Sandbox $sandbox -Path $protected
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$addErrors | Should -HaveCount 1
|
|
|
|
|
$addErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
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 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 the unchanged owner back. Without the Restore privilege, Windows refuses
|
|
|
|
|
# an owner that the user cannot assign, like a file server that refuses the owner (#34): (1307) This security
|
|
|
|
|
# ID may not be assigned as the owner of this object.
|
|
|
|
|
It 'Should add the 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'
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData -ErrorVariable addErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$addErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) |
|
|
|
|
|
Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With inherited entries' {
|
|
|
|
|
# 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, and
|
|
|
|
|
# the cmdlet wrote them back as explicit copies.
|
|
|
|
|
It 'Should add one explicit entry and keep the inherited entries inherited' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inherited'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The page: -PassThru writes all entries, explicit and inherited, of every item that the cmdlet changed.
|
|
|
|
|
Context 'With -PassThru' {
|
|
|
|
|
It 'Should write all entries of the item after the change' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThru'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -PassThru)
|
|
|
|
|
|
|
|
|
|
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAccessRule2] }
|
|
|
|
|
$result | Should -HaveCount @(Get-NTFSAccess -Path $file).Count
|
|
|
|
|
@($result | Where-Object -FilterScript { $_.IsInherited }) | Should -Not -BeNullOrEmpty
|
|
|
|
|
$added = @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and -not $_.IsInherited })
|
|
|
|
|
$added | Should -HaveCount 1
|
|
|
|
|
$added[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write all entries of a security descriptor after the change and leave the item unchanged' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThruDescriptor'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -AccessRights ReadData -PassThru)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount @($sd.SecurityDescriptor.GetAccessRules($true, $true, $sidType)).Count
|
|
|
|
|
@($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and -not $_.IsInherited }) | Should -HaveCount 1
|
|
|
|
|
@((Get-Acl -LiteralPath $file).GetAccessRules($true, $false, $sidType) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With -InheritanceFlags and -PropagationFlags' {
|
|
|
|
|
It 'Should add an entry with the flags to a folder' {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddFlags' -Directory
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -InheritanceFlags ContainerInherit -PropagationFlags InheritOnly
|
|
|
|
|
|
|
|
|
|
$rules = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-32-546' })
|
|
|
|
|
$rules | Should -HaveCount 1
|
|
|
|
|
$rules[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit)
|
|
|
|
|
$rules[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::InheritOnly)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The page: inheritance and propagation flags are ignored on files.
|
|
|
|
|
It 'Should add an entry without flags to a file' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddFlagsFile'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'S-1-5-32-546' -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags InheritOnly
|
|
|
|
|
|
|
|
|
|
$rules = @((Get-Acl -LiteralPath $file).GetAccessRules($true, $false, $sidType) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-32-546' })
|
|
|
|
|
$rules | Should -HaveCount 1
|
|
|
|
|
$rules[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None)
|
|
|
|
|
$rules[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
Describe 'Security descriptor parameter sets' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParameterSets' -Directory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, PowerShell could not choose between the SDSimple and SDComplex parameter sets.
|
|
|
|
|
It '<_> should accept -SecurityDescriptor without -AppliesTo or the flag parameters' -ForEach @(
|
|
|
|
|
'Add-NTFSAccess', 'Remove-NTFSAccess'
|
|
|
|
|
) {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
{ & $_ -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A descriptor from Get-NTFSSecurityDescriptor contains the audit entries only with the Security privilege.
|
|
|
|
|
It '<_> should accept -SecurityDescriptor without -AppliesTo or the flag parameters' -Skip:(-not $holdsSecurityPrivilege) -ForEach @(
|
|
|
|
|
'Add-NTFSAudit', 'Remove-NTFSAudit'
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
) {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
{ & $_ -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Add-NTFSAccess should apply the entry to the folder, its subfolders, and files by default' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$rule = $sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
$rule.InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit')
|
|
|
|
|
$rule.PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Add-NTFSAccess should still take -AppliesTo for a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -AppliesTo ThisFolderOnly
|
|
|
|
|
|
|
|
|
|
$rule = $sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
$rule.InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Clear-NTFSAccess' {
|
|
|
|
|
Context 'With -DisableInheritance' {
|
|
|
|
|
# The cmdlet does not copy the inherited entries, so the item is left with an empty DACL. This is the
|
|
|
|
|
# documented behavior; Set-NTFSInheritance and Disable-NTFSAccessInheritance keep the entries.
|
|
|
|
|
It 'Should leave the item with an empty, protected DACL' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAll'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAccess -Path $file -DisableInheritance
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.AreAccessRulesProtected | Should -BeTrue
|
|
|
|
|
$acl.Access | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
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 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 the unchanged owner back, which Windows refuses without the Restore
|
|
|
|
|
# privilege (#34). For a DACL without the auto-inherit flag, such as that of a new file in the temp folder of
|
|
|
|
|
# the user, Windows returns the owner even when only the DACL is read. Any write of the DACL adds the flag, so
|
|
|
|
|
# the item keeps its DACL as created.
|
|
|
|
|
It 'Should write no error and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearOtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAccess -Path $file -ErrorVariable clearErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$clearErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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 an entry with anything threw an InvalidCastException, so -eq, -contains, and in PowerShell 7
|
|
|
|
|
# also Select-Object -Unique and Compare-Object failed for the output of Get-NTFSAccess. Like the entries of .NET, two
|
|
|
|
|
# objects are equal when they wrap the same entry.
|
|
|
|
|
Describe 'Comparing access entries' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$compareFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Compare'
|
|
|
|
|
Add-NTFSAccess -Path $compareFile -Account 'S-1-1-0' -AccessRights ReadData
|
|
|
|
|
$entries = @(Get-NTFSAccess -Path $compareFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should find an entry equal to itself and not to another entry' {
|
|
|
|
|
$entries.Count | Should -BeGreaterThan 1
|
|
|
|
|
|
|
|
|
|
$entries[0] -eq $entries[0] | Should -BeTrue
|
|
|
|
|
$entries[0] -eq $entries[1] | Should -BeFalse
|
|
|
|
|
$entries -contains $entries[1] | Should -BeTrue
|
|
|
|
|
$entries[0].Equals('S-1-1-0') | Should -BeFalse
|
|
|
|
|
$entries[0].Equals($null) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should work with Select-Object -Unique and Compare-Object' {
|
|
|
|
|
@(@($entries) + $entries[0] | Select-Object -Unique) | Should -HaveCount $entries.Count
|
|
|
|
|
Compare-Object -ReferenceObject $entries -DifferenceObject $entries | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The entry of .NET doesn't know the object of the module, so equality in one direction only would make a hashtable
|
|
|
|
|
# lookup depend on which of the two is the key.
|
|
|
|
|
It 'Should be equal only to an entry of the module, in both directions' {
|
|
|
|
|
$raw = [System.Security.AccessControl.FileSystemAccessRule] $entries[0]
|
|
|
|
|
|
|
|
|
|
$entries[0].Equals($raw) | Should -BeFalse
|
|
|
|
|
$raw.Equals($entries[0]) | 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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, -ExcludeExplicit gave each inherited entry the source of another entry, and Get-NTFSAccess stopped
|
|
|
|
|
# with an ArgumentOutOfRangeException for a security descriptor with audit entries, because it took the sources of the
|
|
|
|
|
# audit entries for the access entries.
|
|
|
|
|
Describe 'InheritedFrom of access entries' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$inheritedFromFile = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFrom'
|
|
|
|
|
Add-NTFSAccess -Path $inheritedFromFile -Account 'S-1-1-0' -AccessRights ReadData
|
|
|
|
|
$expectedSource = @{}
|
|
|
|
|
foreach ($entry in Get-NTFSAccess -Path $inheritedFromFile) {
|
|
|
|
|
if ($entry.IsInherited) {
|
|
|
|
|
$expectedSource["$($entry.Account.Sid)"] = $entry.InheritedFrom
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should name the folder that each inherited entry comes from' {
|
|
|
|
|
$expectedSource.Count | Should -BeGreaterThan 0
|
|
|
|
|
$expectedSource.Values | ForEach-Object -Process { $_ | Should -Not -BeNullOrEmpty }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should name the same folders with -ExcludeExplicit' {
|
|
|
|
|
$result = @(Get-NTFSAccess -Path $inheritedFromFile -ExcludeExplicit)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount $expectedSource.Count
|
|
|
|
|
foreach ($entry in $result) {
|
|
|
|
|
$entry.InheritedFrom | Should -Be $expectedSource["$($entry.Account.Sid)"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Two explicit entries before the inherited ones; before 5.0.0-rc6, -ExcludeExplicit shifted the sources by two.
|
|
|
|
|
It 'Should name the folder of an inheritable entry, also with -ExcludeExplicit' {
|
|
|
|
|
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromParent' -Directory
|
|
|
|
|
Add-NTFSAccess -Path $parent -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles
|
|
|
|
|
$child = Join-Path -Path $parent -ChildPath 'Child.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
|
|
|
|
|
Set-Content -LiteralPath $child -Value 'Child'
|
|
|
|
|
Add-NTFSAccess -Path $child -Account 'S-1-1-0' -AccessRights ReadData
|
|
|
|
|
Add-NTFSAccess -Path $child -Account 'S-1-5-32-545' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$all = @(Get-NTFSAccess -Path $child | Where-Object -FilterScript { $_.IsInherited -and "$($_.Account.Sid)" -eq 'S-1-5-32-546' })
|
|
|
|
|
$inherited = @(Get-NTFSAccess -Path $child -ExcludeExplicit | Where-Object -FilterScript { "$($_.Account.Sid)" -eq 'S-1-5-32-546' })
|
|
|
|
|
|
|
|
|
|
$all | Should -HaveCount 1
|
|
|
|
|
$all[0].InheritedFrom | Should -Be $parent
|
|
|
|
|
$inherited | Should -HaveCount 1
|
|
|
|
|
$inherited[0].InheritedFrom | Should -Be $parent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The module setting GetInheritedFrom turns off the lookup of the sources, which costs a call for each item.
|
|
|
|
|
It 'Should leave InheritedFrom empty when the module setting GetInheritedFrom is off' {
|
|
|
|
|
$saved = $privateData['GetInheritedFrom']
|
|
|
|
|
$privateData['GetInheritedFrom'] = $false
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSAccess -Path $inheritedFromFile -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
$privateData['GetInheritedFrom'] = $saved
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$inherited = @($result | Where-Object -FilterScript { $_.IsInherited })
|
|
|
|
|
$inherited | Should -Not -BeNullOrEmpty
|
|
|
|
|
$inherited | ForEach-Object -Process { $_.InheritedFrom | Should -BeNullOrEmpty }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Windows can't name the folders when the item is gone, for example deleted by another process after its security
|
|
|
|
|
# descriptor was read, or when a folder above it can't be read. The entries still come back. Before 5.0.0, the
|
|
|
|
|
# text lost its last character, and an explicit entry, which has no source, got it as well.
|
|
|
|
|
It 'Should name an unknown parent for an inherited entry and no source for an explicit entry when Windows cannot resolve the folders' {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromGone' -Directory
|
|
|
|
|
$file = Join-Path -Path $folder -ChildPath 'Gone.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
Set-Content -LiteralPath $file -Value 'Gone'
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
|
|
|
|
|
Remove-Item -LiteralPath $file -Force
|
|
|
|
|
|
|
|
|
|
$entries = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true))
|
|
|
|
|
|
|
|
|
|
$inherited = @($entries | Where-Object -FilterScript { $_.IsInherited })
|
|
|
|
|
$inherited | Should -Not -BeNullOrEmpty
|
|
|
|
|
foreach ($entry in $inherited) {
|
|
|
|
|
$entry.InheritedFrom | Should -BeExactly 'unknown parent'
|
|
|
|
|
}
|
|
|
|
|
$explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited })
|
|
|
|
|
$explicit | Should -HaveCount 1
|
|
|
|
|
$explicit[0].InheritedFrom | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
It 'Should read a security descriptor with audit entries and name the same folders' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromAudit'
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
|
|
|
|
|
Add-NTFSAudit -Path $file -Account 'S-1-1-0' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $file
|
|
|
|
|
@($sd.SecurityDescriptor.GetAuditRules($true, $true, $sidType)) | Should -Not -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSAccess -SecurityDescriptor $sd -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount @(Get-NTFSAccess -Path $file).Count
|
|
|
|
|
foreach ($entry in @($result | Where-Object -FilterScript { $_.IsInherited })) {
|
|
|
|
|
$entry.InheritedFrom | Should -Be $expectedSource["$($entry.Account.Sid)"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Describe 'Get-NTFSEffectiveAccess for an unresolved identity' {
|
|
|
|
|
It 'Should report the native identity error for each <Source> and return no access entry' -ForEach @(
|
|
|
|
|
@{ Source = 'Path' }
|
|
|
|
|
@{ Source = 'SecurityDescriptor' }
|
|
|
|
|
) {
|
|
|
|
|
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'UnresolvedFirst'
|
|
|
|
|
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'UnresolvedNext'
|
|
|
|
|
$identity = [Security2.IdentityReference2] 'S-1-5-21-1-2-3-1001'
|
|
|
|
|
$identity.AccountName | Should -BeNullOrEmpty
|
|
|
|
|
$identity.LastError | Should -Not -BeNullOrEmpty
|
|
|
|
|
$before = @((Get-Acl -LiteralPath $first).Sddl, (Get-Acl -LiteralPath $next).Sddl)
|
|
|
|
|
$parameters = @{ Account = $identity; WarningAction = 'SilentlyContinue'; ErrorAction = 'SilentlyContinue' }
|
|
|
|
|
if ($Source -eq 'Path') {
|
|
|
|
|
$parameters.Path = @($first, $next)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$parameters.SecurityDescriptor = @(Get-NTFSSecurityDescriptor -Path $first, $next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess @parameters -ErrorVariable accessErrors)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$accessErrors | Should -HaveCount 2
|
|
|
|
|
for ($index = 0; $index -lt 2; $index++) {
|
|
|
|
|
$accessErrors[$index].FullyQualifiedErrorId | Should -BeLike 'GetEffectiveAccessError,*'
|
|
|
|
|
$accessErrors[$index].CategoryInfo.Category | Should -Be 'ReadError'
|
|
|
|
|
$accessErrors[$index].TargetObject.FullName | Should -BeExactly @($first, $next)[$index]
|
|
|
|
|
$cause = $accessErrors[$index].Exception.GetBaseException()
|
|
|
|
|
$cause | Should -BeOfType [System.ComponentModel.Win32Exception]
|
|
|
|
|
$cause.NativeErrorCode | Should -Be 1332
|
|
|
|
|
}
|
|
|
|
|
(Get-Acl -LiteralPath $first).Sddl | Should -BeExactly $before[0]
|
|
|
|
|
(Get-Acl -LiteralPath $next).Sddl | Should -BeExactly $before[1]
|
|
|
|
|
}
|
|
|
|
|
}
|