<#
Tests how the cmdlets of the module built in NTFSSecurity \ bin \ Release handle the Backup , Restore , Take Ownership ,
and Security privileges . These tests need an access token that holds the privileges , so they skip without them
and run in CI , whose runners are elevated . They change only the privileges of the test process and restore the
module setting EnablePrivileges .
#>
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseDeclaredVarsMoreThanAssignments' , '' , Justification = 'Pester shares variables between blocks.'
) ]
param ( )
BeforeDiscovery {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$missingPrivileges = @ ( 'SeBackupPrivilege' , 'SeRestorePrivilege' , 'SeTakeOwnershipPrivilege' , 'SeSecurityPrivilege' ) |
Where-Object -FilterScript { -not ( Test-PrivilegeHeld -Name $_ ) }
$holdsPrivileges = -not $missingPrivileges
}
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 'Privileges'
Push-Location -LiteralPath $sandbox
$privateData = ( Get-Module -Name NTFSSecurity ) . PrivateData
$enablePrivileges = $privateData [ 'EnablePrivileges' ]
function Get-BackupPrivilegeState {
( Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Backup' ) . PrivilegeState
}
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
function Get-EnabledFileSystemPrivilege {
# The names of the privileges that the cmdlets enable, as far as they are enabled now
@ ( Get-Privileges | Where-Object -FilterScript {
$_ . Privilege -in 'TakeOwnership' , 'Restore' , 'Backup' , 'Security' -and $_ . PrivilegeState -eq 'Enabled'
} | ForEach-Object -Process { $_ . Privilege . ToString ( ) } )
}
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'Disable-Privileges' {
Context 'When the module setting EnablePrivileges is $false' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
# Before 5.0.0, the cmdlet warned that it could not disable the privileges and left them enabled.
It 'Should disable the privileges that Enable-Privileges enabled' -Skip : ( -not $holdsPrivileges ) {
Enable-Privileges
Get-BackupPrivilegeState | Should -Be 'Enabled'
Disable-Privileges -WarningVariable privilegeWarnings -WarningAction SilentlyContinue
$privilegeWarnings | Should -BeNullOrEmpty
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
# Before 5.0.0, the verbose message said that the privileges were now enabled.
It 'Should say in the verbose message that the privileges are disabled' -Skip : ( -not $holdsPrivileges ) {
Enable-Privileges
$messages = Disable-Privileges -Verbose -WarningAction SilentlyContinue 4 > & 1
$messages . Message | Should -Contain " The privileges 'TakeOwnership', 'Restore' and 'Backup' are now disabled. "
}
}
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 access token holds only some of the privileges' {
# Before 5.0.0-rc4, the cmdlet also tried to disable the privileges that the access token doesn't hold, and
# warned for each one that it couldn't disable it. A removed privilege can't be added back, so the test removes
# them in a child process.
It 'Should not warn about the privileges that the access token does not hold' -Skip : ( -not $holdsPrivileges ) {
$script = Join-Path -Path $sandbox -ChildPath 'Disable-PartialPrivileges.ps1'
Assert-TestSandboxPath -Sandbox $sandbox -Path $script
Set-Content -LiteralPath $script -Value @ '
param ( $ModulePath )
Import-Module -Name $ModulePath -ErrorAction Stop
$process = [System.Diagnostics.Process] :: GetCurrentProcess ( )
[ProcessPrivileges.ProcessExtensions] :: RemovePrivilege ( $process , [ProcessPrivileges.Privilege] :: TakeOwnership ) | Out-Null
[ProcessPrivileges.ProcessExtensions] :: RemovePrivilege ( $process , [ProcessPrivileges.Privilege] :: Security ) | Out-Null
Enable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Disable-Privileges -WarningVariable privilegeWarnings -WarningAction SilentlyContinue
'WARNINGS:{0}' -f @ ( $privilegeWarnings ) . Count
' @
$output = & ( Get-Process -Id $PID ) . Path -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $script -ModulePath ( [IO.Path] :: GetFullPath ( $modulePath ) )
$output | Should -Contain 'WARNINGS:0'
}
}
}
Describe 'Inheritance cmdlets' {
Context 'When the module setting EnablePrivileges is $false' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inheritance'
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
# Before 5.0.0, the inheritance cmdlets enabled the privileges anyway and left them enabled.
It '<_> should leave the privileges disabled' -Skip : ( -not $holdsPrivileges ) -ForEach @ (
'Get-NTFSInheritance' , 'Set-NTFSInheritance' , 'Enable-NTFSAccessInheritance' ,
'Disable-NTFSAccessInheritance' , 'Enable-NTFSAuditInheritance' , 'Disable-NTFSAuditInheritance'
) {
Get-BackupPrivilegeState | Should -Be 'Disabled'
& $_ -Path $file -ErrorAction SilentlyContinue | Out-Null
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
}
}
Describe 'Privileges when the pipeline stops early' {
BeforeAll {
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# The cmdlets enable the privileges only with this setting; without it, these tests would prove nothing.
$privateData [ 'EnablePrivileges' ] = $true
$files = 1 . . 3 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name " Stopped $ _ " }
$missing = Join-Path -Path $sandbox -ChildPath 'StoppedMissing.txt'
}
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
# A failing test must not leave the privileges enabled for the tests that follow.
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
# Before 5.0.0-rc6, a cmdlet disabled the privileges that it had enabled only in EndProcessing, which PowerShell
# skips when a later command or a terminating error stops the pipeline. The Backup, Restore, Take Ownership, and
# Security privileges then stayed enabled in the session.
It 'Should disable the privileges after Select-Object -First stops the pipeline' -Skip : ( -not $holdsPrivileges ) {
Get-BackupPrivilegeState | Should -Be 'Disabled'
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$stateWhileRunning = Get-NTFSOwner -Path $files | ForEach-Object -Process { Get-BackupPrivilegeState } |
Select-Object -First 1
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$stateWhileRunning | Should -Be 'Enabled'
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
It 'Should disable the privileges after a terminating error' -Skip : ( -not $holdsPrivileges ) {
Get-BackupPrivilegeState | Should -Be 'Disabled'
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$statesWhileRunning = New-Object -TypeName 'System.Collections.Generic.List[string]'
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
{
Get-NTFSAccess -Path $files [ 0 ] , $missing -ErrorAction Stop |
ForEach-Object -Process { $statesWhileRunning . Add ( ( Get-BackupPrivilegeState ) ) }
} | Should -Throw
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$statesWhileRunning | Should -Not -BeNullOrEmpty
$statesWhileRunning | Should -Not -Contain 'Disabled'
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
It 'Enable-Privileges should keep the privileges enabled also when the pipeline stops early' -Skip : ( -not $holdsPrivileges ) {
Enable-Privileges -PassThru | Select-Object -First 1 | Out-Null
Get-BackupPrivilegeState | Should -Be 'Enabled'
}
}
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# A cmdlet enables the privileges one after the other and writes a debug message before and after each one. A later command
# that takes the debug stream can end the pipeline or throw at the message after the enabling, before the cmdlet has noted
# that it enabled the privilege.
Describe 'Privileges when a later command takes the debug messages of the cmdlet' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $true
$debugFile = New-TestSandboxItem -Sandbox $sandbox -Name 'DebugStopped'
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
# Before 5.0.0-rc7, the privilege that the cmdlet had enabled at that moment stayed enabled in the session: nothing
# disabled it, because the cmdlet had not noted yet that it enabled it.
It 'Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling' -Skip : ( -not $holdsPrivileges ) {
$DebugPreference = 'Continue'
$messages = @ ( Get-NTFSOwner -Path $debugFile 5 > & 1 | ForEach-Object -Process { $_ . Message } )
$enabledAt = $messages . IndexOf ( '..enabled' ) + 1
$enabledAt | Should -BeGreaterThan 0
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
$result = @ ( Get-NTFSOwner -Path $debugFile 5 > & 1 | Select-Object -First $enabledAt )
$result | Should -HaveCount $enabledAt
$result [ - 1 ] . Message | Should -BeExactly '..enabled'
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
# Before 5.0.0-rc7, the cmdlet took the exception for the failure to enable the privilege, went on with the next
# privilege, and the caller never saw it; all four privileges stayed enabled.
It 'Should pass on what a later command throws at the message after the enabling and disable the privileges' -Skip : ( -not $holdsPrivileges ) {
$DebugPreference = 'Continue'
$caught = $null
try {
Get-NTFSOwner -Path $debugFile 5 > & 1 | ForEach-Object -Process {
if ( $_ . Message -eq '..enabled' ) { throw 'Downstream failure' }
$_
} | Out-Null
}
catch {
$caught = $_
}
$caught . Exception . Message | Should -BeLike '*Downstream failure*'
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
}
# Enable-Privileges recognizes the script NTFSSecurity.Init.ps1, which a user adds to start the module, by its name: from
# that script, it enables the privileges only for the module setting EnablePrivileges, from any other script always. Each
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# test runs the script in a child process, which inherits the privilege states of this one (disabled here, see BeforeEach),
# so that the privileges of this process stay as they are. With the setting $true, the module enables the privileges
# itself before the cmdlet runs, so the state alone does not show that the cmdlet did: it also announces that in a verbose
# message.
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Describe 'Enable-Privileges in the script NTFSSecurity.Init.ps1' {
BeforeAll {
function Invoke-StartScript {
param ( [string] $ScriptName , [bool] $Setting )
$folder = Join-Path -Path $sandbox -ChildPath ( 'Start-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
$script = Join-Path -Path $folder -ChildPath $ScriptName
Assert-TestSandboxPath -Sandbox $sandbox -Path $script
New-Item -ItemType Directory -Path $folder | Out-Null
Set-Content -LiteralPath $script -Value @ '
param ( $ModulePath , $Setting )
Import-Module -Name $ModulePath -ErrorAction Stop
( Get-Module -Name NTFSSecurity ) . PrivateData [ 'EnablePrivileges' ] = ( $Setting -eq 'True' )
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$messages = @ ( Enable-Privileges -Verbose 4 > & 1 | ForEach-Object -Process { " $( $_ . Message ) " } )
'ANNOUNCED:{0}' -f [bool] @ ( $messages -like '*are now enabled giving you access*' ) . Count
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
'BACKUP:{0}' -f ( Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Backup' ) . PrivilegeState
' @
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$output = @ ( & ( Get-Process -Id $PID ) . Path -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $script -ModulePath ( [IO.Path] :: GetFullPath ( $modulePath ) ) -Setting $Setting )
[pscustomobject] @ {
Announced = @ ( $output | Where-Object -FilterScript { $_ -like 'ANNOUNCED:*' } ) -replace '^ANNOUNCED:'
Backup = @ ( $output | Where-Object -FilterScript { $_ -like 'BACKUP:*' } ) -replace '^BACKUP:'
}
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
}
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
It 'Should enable the privileges when the module setting EnablePrivileges is $true' -Skip : ( -not $holdsPrivileges ) {
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Get-BackupPrivilegeState | Should -Be 'Disabled'
$result = Invoke-StartScript -ScriptName 'NTFSSecurity.Init.ps1' -Setting $true
$result . Backup | Should -Be 'Enabled'
$result . Announced | Should -Be 'True'
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
It 'Should leave the privileges disabled when the module setting EnablePrivileges is $false' -Skip : ( -not $holdsPrivileges ) {
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Get-BackupPrivilegeState | Should -Be 'Disabled'
$result = Invoke-StartScript -ScriptName 'NTFSSecurity.Init.ps1' -Setting $false
$result . Backup | Should -Be 'Disabled'
$result . Announced | Should -Be 'False'
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
It 'Should enable the privileges in a script of another name also when the module setting EnablePrivileges is $false' -Skip : ( -not $holdsPrivileges ) {
test: make the stream rows independent of the error action, show the Init branch, and pin the dot rules
The independent review of the delta found the tests below weak. The verbose and debug rows of PipelineControl ran their command without an error action; the CI runner sets Stop, under which the handler that reports a later command's exception as an item error ended the pipeline with it, so the mutations that stop recording the verbose or the debug exception escaped (M19 in two configurations, M20 in all four). The rows now say -ErrorAction SilentlyContinue. The Enable-Privileges tests for a script named NTFSSecurity.Init.ps1 could not fail for the branch they name when the module setting was true, because the module enables the privileges itself before the cmdlet runs; they now also assert the verbose message that only the cmdlet writes, and they disable the privileges before each test so that the child process inherits none.
The first-nested-folder test asserts the first folder, not either. Set-TestNullDacl refuses an item that is a link, which the native call would follow out of the sandbox. A table pins the three outcomes that the filter documentation lists for a dot (Report.*, a trailing dot, an empty value), so a change of those rules is a decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Get-BackupPrivilegeState | Should -Be 'Disabled'
$result = Invoke-StartScript -ScriptName 'Other.ps1' -Setting $false
$result . Backup | Should -Be 'Enabled'
$result . Announced | Should -Be 'True'
fix: pass on what a later command raises through the error stream, and disable an interrupted privilege
Get-ChildItem2 swallowed what a later command threw for the error of a nested folder, for example `Get-ChildItem2 -Recurse 2>&1 | ForEach-Object { throw 'x' }`: the recursion took it for a failure of the folder above, wrote a verbose message, and left the loop over the folders, so the listing ended early and the caller never saw the exception. BaseCmdlet now notes the exception that WriteError raises, as it does for WriteObject, WriteVerbose, and WriteDebug. The report that -ErrorAction Stop is swallowed there too was not reproducible (the error reaches the caller), and a break or Select-Object -First was already passed on by type; the tests keep both.
A cmdlet that enables the privileges wrote the debug message "..enabled" before it noted the privilege, so a later command that ended the pipeline or threw at that message left the privilege enabled: Dispose disables only what is noted. TryEnablePrivilege also took the exception of a later command for a failure to enable the privilege and went on with the next one, so `Get-NTFSOwner 5>&1 | ForEach-Object { if ($_.Message -eq '..enabled') { throw 'x' } }` enabled all four privileges and hid the exception. The privilege is noted before the message, and TryEnablePrivilege passes the exception on.
Red before the fix in the Release build of 7aa8315: three tests (the throw on the error stream, in four configurations the first, in the two elevated ones the two privilege tests). Also covers the typed-throw rows that fail if PowerShell stops wrapping a thrown exception, and Enable-Privileges in a script named NTFSSecurity.Init.ps1.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
}
fix: disable the privileges by their current state when a cmdlet ends
A cmdlet that enables the Backup, Restore, Take Ownership, and Security
privileges decided which ones to disable on the states that it had read
when it enabled them, and stopped at the first one that failed. When
another command in the pipeline, such as Disable-Privileges, had disabled
one of them, the cmdlet stopped with "Priviledge already disabled" and
left the privileges after that one enabled. After an early stop, which
Dispose handles since this branch, it left them enabled without any
message, because PowerShell ignores exceptions thrown in Dispose; and
Disable-Privileges threw that exception in Dispose on every call while
the privileges were disabled. 4.2.6 already decided on the old states.
DisablePrivilege now reads the current state, and the cleanup tries every
privilege: in EndProcessing, a privilege that it can't disable gives a
warning; in Dispose, it is ignored.
The early-stop tests now pin EnablePrivileges and check that the cmdlet
had enabled the privileges, so that they can't pass without testing
anything.
Found by the security review of fcb370e..00c3646 (findings 1 to 3).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Describe 'Privileges that another command in the pipeline changes' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $true
$files = 1 . . 3 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name " Changed $ _ " }
function Disable-TakeOwnershipOnce {
# Passes the objects on and disables the Take Ownership privilege when the first one passes, as another
# command in the pipeline can. Records the state of the Backup privilege at that moment.
param (
[ Parameter ( ValueFromPipeline ) ]
[object]
$InputObject ,
[ Parameter ( Mandatory ) ]
[ AllowEmptyCollection ( ) ]
[System.Collections.Generic.List[string]]
$BackupState
)
begin {
$first = $true
}
process {
if ( $first ) {
$BackupState . Add ( ( Get-BackupPrivilegeState ) )
$null = [ProcessPrivileges.ProcessExtensions] :: DisablePrivilege (
[System.Diagnostics.Process] :: GetCurrentProcess ( ) , [ProcessPrivileges.Privilege] :: TakeOwnership
)
$first = $false
}
$InputObject
}
}
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
# Before 5.0.0-rc6, a cmdlet decided which privileges to disable on the states that it had read when it enabled
# them. A privilege that another command had disabled since then stopped it with "Priviledge already disabled",
# and the privileges after that one in its list stayed enabled.
It 'Should disable the other privileges when another command disabled one of them' -Skip : ( -not $holdsPrivileges ) {
$backupState = New-Object -TypeName 'System.Collections.Generic.List[string]'
{ Get-NTFSOwner -Path $files | Disable-TakeOwnershipOnce -BackupState $backupState | Out-Null } | Should -Not -Throw
$backupState | Should -Be 'Enabled'
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
It 'Should disable the other privileges when another command disabled one of them and the pipeline stops early' -Skip : ( -not $holdsPrivileges ) {
$backupState = New-Object -TypeName 'System.Collections.Generic.List[string]'
Get-NTFSOwner -Path $files | Disable-TakeOwnershipOnce -BackupState $backupState | Select-Object -First 1 | Out-Null
$backupState | Should -Be 'Enabled'
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
It 'Should not fail when Disable-Privileges runs inside the pipeline' -Skip : ( -not $holdsPrivileges ) {
{
Get-NTFSOwner -Path $files |
ForEach-Object -Process { Disable-Privileges -WarningAction SilentlyContinue ; $_ } |
Out-Null
} | Should -Not -Throw
Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
}
# The library class of the module that the cmdlets leave unused; the tests change only the privileges of the test process.
Describe 'The PrivilegeEnabler class' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
$backup = [ProcessPrivileges.Privilege] :: Backup
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
$changeNotify = [ProcessPrivileges.Privilege] :: ChangeNotify
$currentProcess = [System.Diagnostics.Process] :: GetCurrentProcess ( )
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 enabler goes out of scope in the function, so that nothing but the caller's handle refers to what it owns.
function New-AbandonedHandle {
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseShouldProcessForStateChangingFunctions' , '' , Justification = 'Test helper that only creates an object.'
) ]
param ( $Process )
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $Process
$field = [ProcessPrivileges.PrivilegeEnabler] . GetField ( 'accessTokenHandle' , [System.Reflection.BindingFlags] 'NonPublic, Instance' )
$field . GetValue ( $enabler )
}
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
It 'Should enable a disabled privilege until it is disposed' -Skip : ( -not $holdsPrivileges ) {
Get-BackupPrivilegeState | Should -Be 'Disabled'
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess , $backup
try {
Get-BackupPrivilegeState | Should -Be 'Enabled'
}
finally {
$enabler . Dispose ( )
}
Get-BackupPrivilegeState | Should -Be 'Disabled'
$enabler . Dispose ( )
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
It 'Should report a privilege that it modified once and leave it to the instance that enabled it' -Skip : ( -not $holdsPrivileges ) {
$first = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess
$second = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess
try {
$first . EnablePrivilege ( $backup ) | Should -Be 'PrivilegeModified'
Get-BackupPrivilegeState | Should -Be 'Enabled'
$first . EnablePrivilege ( $backup ) | Should -Be 'None'
$second . EnablePrivilege ( $backup ) | Should -Be 'None'
$second . Dispose ( )
Get-BackupPrivilegeState | Should -Be 'Enabled'
}
finally {
$first . Dispose ( )
$second . Dispose ( )
}
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
It 'Should not disable a privilege that was enabled before' -Skip : ( -not $holdsPrivileges ) {
$null = [ProcessPrivileges.ProcessExtensions] :: EnablePrivilege ( $currentProcess , $backup )
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess , $backup
try {
$enabler . EnablePrivilege ( $backup ) | Should -Be 'None'
}
finally {
$enabler . Dispose ( )
}
Get-BackupPrivilegeState | Should -Be 'Enabled'
}
It 'Should enable a privilege through an access token handle that the caller owns' -Skip : ( -not $holdsPrivileges ) {
$rights = [ProcessPrivileges.TokenAccessRights] :: AdjustPrivileges -bor [ProcessPrivileges.TokenAccessRights] :: Query
$handle = [ProcessPrivileges.ProcessExtensions] :: GetAccessTokenHandle ( $currentProcess , $rights )
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
$enabler = $null
try {
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $handle , $backup
Get-BackupPrivilegeState | Should -Be 'Enabled'
$enabler . Dispose ( )
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
$enabler = $null
Get-BackupPrivilegeState | Should -Be 'Disabled'
$handle . IsClosed | Should -BeFalse
}
finally {
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 enabler first: a handle that is closed under an enabler that still owns a privilege fails when the
# enabler disables the privilege.
if ( $enabler ) {
$enabler . Dispose ( )
}
$handle . Dispose ( )
}
$handle . IsClosed | Should -BeTrue
}
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 finalizer closes the token handle that an abandoned enabler opened and drops its registration, so that the next
# enabler for the process opens a handle of its own instead of taking a closed one. The handle is private, so the test
# reads it by reflection. An enabler that enabled a privilege stays referenced by a static list until it is disposed,
# so it is never finalized and its privilege stays enabled; only an enabler without a privilege can be abandoned.
It 'Should close the token handle of an enabler that was never disposed when it is finalized' {
$handle = New-AbandonedHandle -Process $currentProcess
$handle . IsClosed | Should -BeFalse
for ( $attempt = 0 ; $attempt -lt 10 -and -not $handle . IsClosed ; $attempt + + ) {
[GC] :: Collect ( )
[GC] :: WaitForPendingFinalizers ( )
}
$handle . IsClosed | Should -BeTrue
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess
try {
$enabler . EnablePrivilege ( $changeNotify ) | Should -Be 'None'
}
finally {
$enabler . Dispose ( )
}
}
# The access tokens of administrators don't hold the privilege to create a token, and those of basic users don't hold
# most of the others.
It 'Should leave a privilege that the access token does not hold alone' {
$removed = [ProcessPrivileges.Privilege] :: CreateToken
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $currentProcess , $removed ) | Should -Be 'Removed'
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess
try {
$enabler . EnablePrivilege ( $removed ) | Should -Be 'None'
}
finally {
$enabler . Dispose ( )
}
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $currentProcess , $removed ) | Should -Be 'Removed'
}
# The enabled flag decides first, then the removed flag; the attributes are not a flags enumeration in .NET.
It 'Should derive the state <Expected> from the attribute value <Value>' -ForEach @ (
@ { Value = 0 ; Expected = 'Disabled' }
@ { Value = 1 ; Expected = 'Disabled' }
@ { Value = 2 ; Expected = 'Enabled' }
@ { Value = 3 ; Expected = 'Enabled' }
@ { Value = 4 ; Expected = 'Removed' }
@ { Value = 6 ; Expected = 'Enabled' }
@ { Value = - 2147483648 ; Expected = 'Disabled' }
) {
$attributes = [Enum] :: ToObject ( [ProcessPrivileges.PrivilegeAttributes] , $Value )
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $attributes ) | Should -Be $Expected
}
}
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
# Every access token holds the privilege to bypass traverse checking, enabled. The tests use it because they need no other
# privilege and change nothing: a handle that lacks a right fails before it adjusts anything.
Describe 'The access token handle of a process' {
BeforeAll {
$currentProcess = [System.Diagnostics.Process] :: GetCurrentProcess ( )
$changeNotify = [ProcessPrivileges.Privilege] :: ChangeNotify
$tokenRights = [ProcessPrivileges.TokenAccessRights]
}
It 'Should open a handle with all access rights when the caller names none and close it on dispose' {
$handle = [ProcessPrivileges.ProcessExtensions] :: GetAccessTokenHandle ( $currentProcess )
try {
$handle . IsInvalid | Should -BeFalse
@ ( [ProcessPrivileges.ProcessExtensions] :: GetPrivileges ( $handle ) ) | Should -Not -BeNullOrEmpty
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $handle , $changeNotify ) | Should -Be 'Enabled'
}
finally {
$handle . Dispose ( )
}
$handle . IsClosed | Should -BeTrue
}
It 'Should refuse to enable a privilege through a handle that may only query' {
$handle = [ProcessPrivileges.ProcessExtensions] :: GetAccessTokenHandle ( $currentProcess , $tokenRights :: Query )
try {
$failure = { [ProcessPrivileges.ProcessExtensions] :: EnablePrivilege ( $handle , $changeNotify ) } | Should -Throw -PassThru
$failure . Exception . InnerException | Should -BeOfType [System.ComponentModel.Win32Exception]
$failure . Exception . InnerException . NativeErrorCode | Should -Be 5
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $handle , $changeNotify ) | Should -Be 'Enabled'
}
finally {
$handle . Dispose ( )
}
}
It 'Should refuse to <Operation> through a handle that may only adjust privileges' -ForEach @ (
@ { Operation = 'list the privileges' }
@ { Operation = 'read the state of a privilege' }
) {
$handle = [ProcessPrivileges.ProcessExtensions] :: GetAccessTokenHandle ( $currentProcess , $tokenRights :: AdjustPrivileges )
try {
$failure = {
if ( $Operation -eq 'list the privileges' ) {
[ProcessPrivileges.ProcessExtensions] :: GetPrivileges ( $handle )
}
else {
[ProcessPrivileges.ProcessExtensions] :: GetPrivilegeState ( $handle , $changeNotify )
}
} | Should -Throw -PassThru
$failure . Exception . InnerException | Should -BeOfType [System.ComponentModel.Win32Exception]
$failure . Exception . InnerException . NativeErrorCode | Should -Be 5
}
finally {
$handle . Dispose ( )
}
}
}
Describe 'The PrivilegeControl class' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
$control = New-Object -TypeName 'Security2.PrivilegeControl'
$backup = [ProcessPrivileges.Privilege] :: Backup
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
It 'Should refuse to <Operation> a privilege that the access token does not hold' -ForEach @ (
@ { Operation = 'enable' }
@ { Operation = 'disable' }
) {
$failure = {
if ( $Operation -eq 'enable' ) {
$control . EnablePrivilege ( [ProcessPrivileges.Privilege] :: CreateToken )
}
else {
$control . DisablePrivilege ( [ProcessPrivileges.Privilege] :: CreateToken )
}
} | Should -Throw -PassThru
$failure . Exception . InnerException | Should -BeOfType [System.Security.AccessControl.PrivilegeNotHeldException]
$failure . Exception . InnerException . PrivilegeName | Should -BeExactly 'CreateToken'
}
It 'Should enable and disable a held privilege and refuse to repeat either' -Skip : ( -not $holdsPrivileges ) {
Get-BackupPrivilegeState | Should -Be 'Disabled'
$failure = { $control . DisablePrivilege ( $backup ) } | Should -Throw -PassThru
$failure . Exception . InnerException | Should -BeOfType [Security2.AdjustPriviledgeException]
$failure . Exception . InnerException . Message | Should -BeExactly 'Priviledge already disabled'
$control . EnablePrivilege ( $backup ) | Should -Be 'PrivilegeModified'
Get-BackupPrivilegeState | Should -Be 'Enabled'
$failure = { $control . EnablePrivilege ( $backup ) } | Should -Throw -PassThru
$failure . Exception . InnerException | Should -BeOfType [Security2.AdjustPriviledgeException]
$failure . Exception . InnerException . Message | Should -BeExactly 'Priviledge already enabled'
$control . DisablePrivilege ( $backup ) | Should -Be 'PrivilegeModified'
Get-BackupPrivilegeState | Should -Be 'Disabled'
}
}
fix(privileges): report a malformed EnablePrivileges setting with one clear error
BaseCmdletWithPrivControl.BeginProcessing and EndProcessing read
(bool)privateData["EnablePrivileges"] without checking it first; a
setting that is not a Boolean, such as the string '$true' from a typo
in the module manifest, or a missing key, made every cmdlet derived
from the class fail with an InvalidCastException or a
NullReferenceException before anything useful was said.
Enable-Privileges had its own try/catch for the same cast that raised a
different message (a ParseException), but it was unreachable, because
BeginProcessing always failed first.
A new helper, GetEnablePrivilegesSetting, centralizes the check in the
base class: a setting that is missing or not a Boolean now stops the
cmdlet with one terminating error, id EnablePrivilegesSettingInvalid,
category InvalidData, that names the setting, says it must be $true or
$false, and names the module manifest. BeginProcessing, EndProcessing,
and Enable-Privileges all use it now; the dead try/catch of
Enable-Privileges is removed, so a mistaken setting no longer produces
two different messages depending on which cmdlet hit it first. The
valid settings $true and $false keep today's behavior.
GetInheritedFrom (used by Get-NTFSAccess and Get-NTFSAudit),
GetFileSystemModeProperty, and IdentifyHardLinks (both used by
Get-ChildItem2) read their own setting the same unchecked way and share
this weakness; they are not changed here; see the final report.
Breaking: a script that caught the old InvalidCastException,
NullReferenceException, or ParseException for a malformed
EnablePrivileges setting now needs to catch the terminating error with
id EnablePrivilegesSettingInvalid instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
# Before 5.0.0, a module setting that was not a Boolean (a typo such as the string '$true', or a missing key) made
# every cmdlet derived from BaseCmdletWithPrivControl fail in BeginProcessing with an InvalidCastException or a
# NullReferenceException before anything useful was said, and Enable-Privileges had its own try/catch for the same
# cast that never ran, because BeginProcessing always failed first. One EnablePrivilegesSettingInvalid error now
# names the setting instead, for a cmdlet and for Enable-Privileges alike. The test needs no elevation.
Describe 'A malformed EnablePrivileges setting' {
BeforeAll {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'MalformedSetting'
}
It 'Should stop a cmdlet and Enable-Privileges with one EnablePrivilegesSettingInvalid error for <Case>, not an InvalidCastException or a NullReferenceException' -ForEach @ (
@ { Case = 'a string value' ; Setting = 'maybe' }
@ { Case = 'a missing value' ; Setting = $null }
) {
$privateData [ 'EnablePrivileges' ] = $Setting
try {
$cmdletFailure = { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Throw -PassThru
$enableFailure = { Enable-Privileges -ErrorAction Stop } | Should -Throw -PassThru
}
finally {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
foreach ( $failure in $cmdletFailure , $enableFailure ) {
$failure . FullyQualifiedErrorId | Should -BeLike 'EnablePrivilegesSettingInvalid,*'
$failure . Exception . Message | Should -BeLike '*EnablePrivileges*'
$failure . Exception | Should -Not -BeOfType [System.InvalidCastException]
$failure . Exception | Should -Not -BeOfType [System.NullReferenceException]
}
# Restoring the setting must leave the session usable again, for this test and the ones that follow it.
$privateData [ 'EnablePrivileges' ] | Should -Be $enablePrivileges
{ Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Not -Throw
}
}