From c00bc6af881e58fe85b880da91b202eddba2c118 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 00:40:15 +0200 Subject: [PATCH] fix: let a stopped pipeline end Get-NTFSOwner without errors Defect 9, both parts: - Get-NTFSOwner wrote each owner inside a try block whose catch-all turned every exception into a ReadSecurityError. When a command such as Select-Object -First 1 stopped the pipeline, the cmdlet wrote "The pipeline has been stopped" as an error for every path, and the stop escaped into the caller's script. The owner is now written after the try block. - After access was denied, the retry called the same failing GetOwner again before taking ownership, so it always failed and reported a WriteError. Taking ownership would also replace the owner that the cmdlet reports. The cmdlet now writes a ReadSecurityError with the category PermissionDenied and continues. Tests/Owner.Tests.ps1 (new): 2 tests; the access-denied test skips where the Backup privilege may bypass the deny entry (CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 4 ++ Docs/Cmdlets/Get-NTFSOwner.md | 4 ++ NTFSSecurity/OwnerCmdlets/GetOwner.cs | 31 ++++------- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 2 + Tests/Owner.Tests.ps1 | 54 ++++++++++++++++++++ 5 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 Tests/Owner.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e42d2..814b06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,5 +77,9 @@ The format is based on - Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported `AuditInheritanceEnabled` as `$true` for a security descriptor that was read without its audit section; it now reports `$null`, like `-Path` +- Fix `Get-NTFSOwner`, which wrote a "The pipeline has been stopped" error + for every path when a command such as `Select-Object -First 1` stopped the + pipeline, and which repeated a failed read instead of reporting the + denied access [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Get-NTFSOwner.md b/Docs/Cmdlets/Get-NTFSOwner.md index bd23beb..ccafc18 100644 --- a/Docs/Cmdlets/Get-NTFSOwner.md +++ b/Docs/Cmdlets/Get-NTFSOwner.md @@ -127,6 +127,10 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva The module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well. +If the owner of an item cannot be read because access is denied, the cmdlet writes the non-terminating error `ReadSecurityError` with the category `PermissionDenied` and continues with the next path. It does not take ownership of the item, which would replace the owner that it reports. + +Before 5.0.0, a command that stopped the pipeline early, such as `Select-Object -First 1`, made the cmdlet write a `ReadSecurityError` with the message "The pipeline has been stopped" for every path. + ## RELATED LINKS [Set-NTFSOwner](Set-NTFSOwner.md) diff --git a/NTFSSecurity/OwnerCmdlets/GetOwner.cs b/NTFSSecurity/OwnerCmdlets/GetOwner.cs index b19e259..3c7b524 100644 --- a/NTFSSecurity/OwnerCmdlets/GetOwner.cs +++ b/NTFSSecurity/OwnerCmdlets/GetOwner.cs @@ -43,10 +43,11 @@ namespace NTFSSecurity.OwnerCmdlets { if (ParameterSetName == "Path") { - FileSystemInfo item = null; - foreach (var path in paths) { + FileSystemInfo item = null; + FileSystemOwner owner = null; + try { item = GetFileSystemInfo2(path); @@ -59,32 +60,22 @@ namespace NTFSSecurity.OwnerCmdlets try { - WriteObject(FileSystemOwner.GetOwner(item)); + owner = FileSystemOwner.GetOwner(item); } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException ex) { - try - { - var ownerInfo = FileSystemOwner.GetOwner(item); - var previousOwner = ownerInfo.Owner; - - FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User); - - WriteObject(FileSystemOwner.GetOwner(item)); - - FileSystemOwner.SetOwner(item, previousOwner); - } - catch (Exception ex2) - { - WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); - continue; - } + // Taking ownership to read the owner would replace the owner that the cmdlet reports. + WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.PermissionDenied, path)); + continue; } catch (Exception ex) { WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); continue; } + + // Outside the try block, so that a stopped pipeline isn't reported as a read error. + WriteObject(owner); } } else diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 57ec507..c777abf 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -6146,6 +6146,8 @@ PS C:\> $orphaned | Select-Object FullName, Account, AccessRights, AuditFlags When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. The module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well. + If the owner of an item cannot be read because access is denied, the cmdlet writes the non-terminating error `ReadSecurityError` with the category `PermissionDenied` and continues with the next path. It does not take ownership of the item, which would replace the owner that it reports. + Before 5.0.0, a command that stopped the pipeline early, such as `Select-Object -First 1`, made the cmdlet write a `ReadSecurityError` with the message "The pipeline has been stopped" for every path. diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 new file mode 100644 index 0000000..9c5d993 --- /dev/null +++ b/Tests/Owner.Tests.ps1 @@ -0,0 +1,54 @@ +<# + Tests the owner 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 Backup privilege, Windows may grant reading the owner despite a deny entry. + $canBypassDeny = Test-PrivilegeHeld -Name 'SeBackupPrivilege' +} + +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 'Owner' + Push-Location -LiteralPath $sandbox +} + +AfterAll { + Pop-Location + Remove-TestSandbox -Sandbox $sandbox + Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue +} + +Describe 'Get-NTFSOwner' { + Context 'When a downstream command stops the pipeline' { + It 'Should stop without writing errors' { + $files = 1..3 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "Owner$_" } + + $result = @(Get-NTFSOwner -Path $files -ErrorVariable ownerErrors -ErrorAction SilentlyContinue | Select-Object -First 1) + + $ownerErrors | Should -BeNullOrEmpty + $result | Should -HaveCount 1 + } + } + + Context 'When the owner cannot be read' { + It 'Should write one permission error and keep the owner' -Skip:$canBypassDeny { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied' + Block-TestReadPermission -Sandbox $sandbox -Path $file + + $result = @(Get-NTFSOwner -Path $file -ErrorVariable ownerErrors -ErrorAction SilentlyContinue) + + $result | Should -BeNullOrEmpty + $ownerErrors | Should -HaveCount 1 + $ownerErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $ownerErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied' + } + } +}