From 3899228508ecc7cfcd5253f29ca559c67b0e9f75 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Thu, 8 Oct 2026 15:36:10 +0000 Subject: [PATCH] fix(audit): write a ReadSecurityError in Get-NTFSOrphanedAudit Without the Security privilege, Get-NTFSOrphanedAudit read the item without its SACL and returned nothing, which looked like an item without orphaned entries, and it wrote a warning for an item that it couldn't read. It now reads only the SACL, like Get-NTFSAudit, and writes a ReadSecurityError with the category PermissionDenied or OpenError. The error for a path that doesn't exist stays ReadError. Decision 22, item 1: an assumption in autopilot, flagged for the maintainer's review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 4 ++++ Docs/Cmdlets/Get-NTFSOrphanedAudit.md | 8 ++++--- .../AuditCmdlets/Get-OrphanedAudit.cs | 12 ++++++++-- NTFSSecurity/AuditCmdlets/GetAudit.cs | 6 +++-- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 7 +++--- Tests/Audit.Tests.ps1 | 24 +++++++++++++++---- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9659ecb..e650308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -328,5 +328,9 @@ The format is based on entry, and `Get-NTFSAccess -SecurityDescriptor` stopped with an `ArgumentOutOfRangeException` for a descriptor with audit entries, such as one that `Get-NTFSSecurityDescriptor` reads in an elevated session +- Fix `Get-NTFSOrphanedAudit`, which returned nothing without the Security + privilege, as for an item without orphaned entries, and wrote a warning + for an item that it couldn't read; it now writes a `ReadSecurityError`, + like `Get-NTFSAudit` [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Get-NTFSOrphanedAudit.md b/Docs/Cmdlets/Get-NTFSOrphanedAudit.md index 59a3229..d96682a 100644 --- a/Docs/Cmdlets/Get-NTFSOrphanedAudit.md +++ b/Docs/Cmdlets/Get-NTFSOrphanedAudit.md @@ -163,7 +163,7 @@ You can pipe paths to this cmdlet, or objects that have a `Path` or `FullName` p ### Security2.FileSystemSecurity2[] -Security descriptors bind to the inherited `-SecurityDescriptor` parameter, but this cmdlet does not read their audit entries. +Security descriptors that `Get-NTFSSecurityDescriptor` returned bind to `-SecurityDescriptor`, and the cmdlet examines their audit entries. ### Security2.IdentityReference2 @@ -179,12 +179,14 @@ The cmdlet returns the audit entries whose account SID cannot be translated into 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. -Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it the cmdlet reads the security descriptor without its SACL and reports no orphaned entries at all, which looks the same as a tree that has none. +Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes the non-terminating error `ReadSecurityError` for each item, which reports "A required privilege is not held by the client", like `Get-NTFSAudit`. -If an item cannot be read, the cmdlet writes a warning and continues with the next item. Unlike `Get-NTFSAudit`, it does not try to take ownership of the item when access is denied. +If the audit entries of an item can't be read, the cmdlet writes a `ReadSecurityError`, with the category `PermissionDenied` when access is denied, and continues with the next item; for a path that doesn't exist, it writes a `ReadError`. Like `Get-NTFSAudit`, it doesn't take ownership of the item, because ownership grants no access to the SACL. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection. +Before 5.0.0-rc7, the cmdlet read an item without its SACL when the Security privilege was missing and reported no orphaned entries, which looked the same as an item that has none. For an item that it couldn't read, it wrote a warning instead of an error. + ## RELATED LINKS [Get-NTFSAudit](Get-NTFSAudit.md) diff --git a/NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs b/NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs index fe1c43d..02fbaf8 100644 --- a/NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs +++ b/NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs @@ -46,13 +46,21 @@ namespace NTFSSecurity.AuditCmdlets IEnumerable acl = null; + // Only the SACL, like Get-NTFSAudit, which fails without the Security privilege. Before 5.0.0-rc7, the + // cmdlet read the item without its audit entries then and returned nothing, as for an item without + // orphaned entries, and it wrote a warning for an item that it couldn't read. try { - acl = FileSystemAuditRule2.GetFileSystemAuditRules(item, !ExcludeExplicit, !ExcludeInherited, getInheritedFrom); + acl = GetAuditRules(item); + } + catch (UnauthorizedAccessException ex) + { + this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.PermissionDenied, p)); + continue; } catch (Exception ex) { - this.WriteWarning(string.Format("Could not read item {0}. The error was: {1}", p, ex.Message)); + this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, p)); continue; } diff --git a/NTFSSecurity/AuditCmdlets/GetAudit.cs b/NTFSSecurity/AuditCmdlets/GetAudit.cs index f132702..324247c 100644 --- a/NTFSSecurity/AuditCmdlets/GetAudit.cs +++ b/NTFSSecurity/AuditCmdlets/GetAudit.cs @@ -130,9 +130,11 @@ namespace NTFSSecurity } } - private IEnumerable GetAuditRules(FileSystemInfo item) + /// + /// Reads only the SACL of an item. Without the Security privilege, this fails instead of returning no entries. + /// + protected IEnumerable GetAuditRules(FileSystemInfo item) { - // Reading only the SACL fails without the Security privilege, instead of returning no entries. var sd = new FileSystemSecurity2(item, System.Security.AccessControl.AccessControlSections.Audit); return FileSystemAuditRule2.GetFileSystemAuditRules(sd, !excludeExplicit, !excludeInherited, getInheritedFrom); } diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index f8e25a2..80f5293 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -5988,7 +5988,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd Security2.FileSystemSecurity2[] - Security descriptors bind to the inherited `-SecurityDescriptor` parameter, but this cmdlet does not read their audit entries. + Security descriptors that `Get-NTFSSecurityDescriptor` returned bind to `-SecurityDescriptor`, and the cmdlet examines their audit entries. @@ -6013,9 +6013,10 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd 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. - Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it the cmdlet reads the security descriptor without its SACL and reports no orphaned entries at all, which looks the same as a tree that has none. - If an item cannot be read, the cmdlet writes a warning and continues with the next item. Unlike `Get-NTFSAudit`, it does not try to take ownership of the item when access is denied. + Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes the non-terminating error `ReadSecurityError` for each item, which reports "A required privilege is not held by the client", like `Get-NTFSAudit`. + If the audit entries of an item can't be read, the cmdlet writes a `ReadSecurityError`, with the category `PermissionDenied` when access is denied, and continues with the next item; for a path that doesn't exist, it writes a `ReadError`. Like `Get-NTFSAudit`, it doesn't take ownership of the item, because ownership grants no access to the SACL. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection. + Before 5.0.0-rc7, the cmdlet read an item without its SACL when the Security privilege was missing and reported no orphaned entries, which looked the same as an item that has none. For an item that it couldn't read, it wrote a warning instead of an error. diff --git a/Tests/Audit.Tests.ps1 b/Tests/Audit.Tests.ps1 index fc99457..f9fa2a8 100644 --- a/Tests/Audit.Tests.ps1 +++ b/Tests/Audit.Tests.ps1 @@ -242,7 +242,7 @@ Describe 'Get-NTFSOrphanedAudit' { } } - It 'Should write an error for a path that does not exist and continue with the next path' { + It 'Should write an error for a path that does not exist and continue with the next path' -Skip:(-not $canReadAudit) { $result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) $orphanedErrors | Should -HaveCount 1 @@ -251,6 +251,19 @@ Describe 'Get-NTFSOrphanedAudit' { $result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile } } + # Since 5.0.0-rc7, the next path has an error of its own without the Security privilege, which shows that the cmdlet + # continued with it. + It 'Should write an error for a path that does not exist and continue with the next path without the Security privilege' -Skip:$canReadAudit { + $result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) + + $result | Should -BeNullOrEmpty + $orphanedErrors | Should -HaveCount 2 + $orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*' + $orphanedErrors[0].TargetObject | Should -Be $missing + $orphanedErrors[1].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $orphanedErrors[1].TargetObject | Should -Be $orphanedFile + } + It 'Should write an error for a security descriptor that was read without the audit entries' { $sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList ( (Get-Item2 -Path $orphanedFile), [System.Security.AccessControl.AccessControlSections]::Access @@ -263,12 +276,15 @@ Describe 'Get-NTFSOrphanedAudit' { $orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' } - # The cmdlet page: without the privilege, the cmdlet reads no audit entries and reports none. - It 'Should return nothing and write no error without the Security privilege' -Skip:$canReadAudit { + # Before 5.0.0-rc7, the cmdlet read the item without its audit entries and returned nothing, as for an item without + # orphaned entries; it now writes the error of Get-NTFSAudit. + It 'Should write a ReadSecurityError without the Security privilege instead of returning nothing' -Skip:$canReadAudit { $result = @(Get-NTFSOrphanedAudit -Path $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) - $orphanedErrors | Should -BeNullOrEmpty $result | Should -BeNullOrEmpty + $orphanedErrors | Should -HaveCount 1 + $orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $orphanedErrors[0].TargetObject | Should -Be $orphanedFile } }