From 962887aa6f842938a85c34c5c68378ff55da102c Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sat, 10 Oct 2026 00:46:30 +0000 Subject: [PATCH] fix(security): report one audit inheritance by path and by descriptor The module's own tests ran on Windows Server 2019, 2022, and 2025 and on Windows 11 in the operating-system matrix. Two tests failed on every domain-joined machine and passed on the development host: - Get-NTFSInheritance -SecurityDescriptor reported AuditInheritanceEnabled as $false for an item without audit entries, where -Path reported $true. On these computers Windows reports the SACL as protected from inheritance when it reads all sections together, and as not protected when it reads the SACL alone. The descriptor now takes the audit section from a separate read, like it already did for the access section. Write() stores the sections that were read, so a descriptor with the wrong flag would also have written the SACL back as protected. - Get-NTFSEffectiveAccess -ServerName '' wrote an "Access is denied" error on computers where Windows takes an empty name for this computer. An empty name no longer asks the remote interface of the authorization manager; the cmdlet warns and returns the result of this computer, like for any name that can't be reached. The existing tests that found them stay as the regression guards: they were red on three virtual machines (Windows Server 2022 and 2025, Windows 11) and are green after the change. Three tests are new: the audit state of a file and a folder without audit entries, and of a file and a folder with disabled audit inheritance, by path and by descriptor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 14 ++++++ Security2/FileSystem/FileSystemSecurity2.cs | 8 +++ Security2/Win32/Lib.cs | 54 ++++++++++++--------- Tests/Inheritance.Tests.ps1 | 31 ++++++++++++ 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d03e8..1ddb190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,20 @@ 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-NTFSInheritance -SecurityDescriptor` for an item without audit + entries on computers where Windows reports its audit entries as protected + from inheritance when it reads all sections of the security descriptor at + once, as it did on domain-joined Windows Server 2022 and 2025 and on + Windows 11: the cmdlet reported `AuditInheritanceEnabled` as `$false`, + while `-Path` reported `$true`. The descriptor now takes the state of the + audit entries from a read of that section alone, like `-Path`, and the + cmdlets that start from such a descriptor no longer see the audit entries + as protected +- Fix `Get-NTFSEffectiveAccess -ServerName ''`, which wrote an "Access is + denied" error instead of the warning for a computer that can't be reached, + on computers where Windows takes an empty name for this one. An empty name + never asks the remote interface of the authorization manager now: the + cmdlet warns and returns the result of this computer on every computer - 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 diff --git a/Security2/FileSystem/FileSystemSecurity2.cs b/Security2/FileSystem/FileSystemSecurity2.cs index b984d7d..cf905ba 100644 --- a/Security2/FileSystem/FileSystemSecurity2.cs +++ b/Security2/FileSystem/FileSystemSecurity2.cs @@ -66,10 +66,18 @@ namespace Security2 // Read together with the SACL, the inherited entries of a DACL without the auto-inherit flag lose their // inherited flag when the parent folder has no SACL, and writing such a DACL back stores them as explicit // entries. Read alone, the DACL keeps the flags. + // + // The same goes for the SACL: read together with the other sections, the SACL of an item without audit + // entries is reported as protected from inheritance on some computers (seen on domain-joined Windows Server + // 2022 and 2025 and on Windows 11), while the read of the SACL alone, which Get-NTFSInheritance uses for a + // path, reports it as not protected. The state of the item has to be the same by path and by descriptor. if (HasAuditSection) { var accessSecurity = GetSecurity(item, AccessControlSections.Access); sd.SetSecurityDescriptorBinaryForm(accessSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Access); + + var auditSecurity = GetSecurity(item, AccessControlSections.Audit); + sd.SetSecurityDescriptorBinaryForm(auditSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Audit); } RememberSections(); diff --git a/Security2/Win32/Lib.cs b/Security2/Win32/Lib.cs index 957584b..dd572de 100644 --- a/Security2/Win32/Lib.cs +++ b/Security2/Win32/Lib.cs @@ -177,16 +177,26 @@ namespace Security2 { remoteServerAvailable = false; - var rpcInitInfo = new AUTHZ_RPC_INIT_INFO_CLIENT(); + // An empty name names no computer. Windows takes it for this computer on some computers, where the remote + // interface then refuses the check with "Access is denied", and for an unreachable one on others. So the + // remote interface isn't asked, and the local authorization manager calculates the result, like for any + // name that can't be reached. + if (!string.IsNullOrWhiteSpace(serverName)) + { + var rpcInitInfo = new AUTHZ_RPC_INIT_INFO_CLIENT(); - rpcInitInfo.version = AuthzRpcClientVersion.V1; - rpcInitInfo.objectUuid = AUTHZ_OBJECTUUID_WITHCAP; - rpcInitInfo.protocol = RCP_OVER_TCP_PROTOCOL; - rpcInitInfo.server = serverName; + rpcInitInfo.version = AuthzRpcClientVersion.V1; + rpcInitInfo.objectUuid = AUTHZ_OBJECTUUID_WITHCAP; + rpcInitInfo.protocol = RCP_OVER_TCP_PROTOCOL; + rpcInitInfo.server = serverName; + + SafeHGlobalHandle pRpcInitInfo = SafeHGlobalHandle.AllocHGlobalStruct(rpcInitInfo); + if (AuthzInitializeRemoteResourceManager(pRpcInitInfo.ToIntPtr(), out authzRM)) + { + remoteServerAvailable = true; + return; + } - SafeHGlobalHandle pRpcInitInfo = SafeHGlobalHandle.AllocHGlobalStruct(rpcInitInfo); - if (!AuthzInitializeRemoteResourceManager(pRpcInitInfo.ToIntPtr(), out authzRM)) - { int error = Marshal.GetLastWin32Error(); // The computer can't be resolved or reached (RPC server unavailable), or it doesn't offer the remote @@ -203,24 +213,20 @@ namespace Security2 { remoteServerAvailable = true; } - - // - // As a fallback we do AuthzInitializeResourceManager. But the results can be inaccurate. - // - if (!AuthzInitializeResourceManager( - AuthzResourceManagerFlags.NO_AUDIT, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - "EffectiveAccessCheck", - out authzRM)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } } - else + + // + // As a fallback we do AuthzInitializeResourceManager. But the results can be inaccurate. + // + if (!AuthzInitializeResourceManager( + AuthzResourceManagerFlags.NO_AUDIT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + "EffectiveAccessCheck", + out authzRM)) { - remoteServerAvailable = true; + throw new Win32Exception(Marshal.GetLastWin32Error()); } } diff --git a/Tests/Inheritance.Tests.ps1 b/Tests/Inheritance.Tests.ps1 index e50746d..d961598 100644 --- a/Tests/Inheritance.Tests.ps1 +++ b/Tests/Inheritance.Tests.ps1 @@ -59,6 +59,37 @@ Describe 'Get-NTFSInheritance' { $bySecurityDescriptor.AuditInheritanceEnabled | Should -Be $byPath.AuditInheritanceEnabled } + # Windows reports the SACL of an item without audit entries as protected from inheritance on some computers when it + # reads all sections together, and as not protected when it reads the SACL alone (domain-joined Windows Server 2022 + # and 2025, Windows 11). The state by descriptor has to follow the state of the item. + It 'Should report the same state as for the path of a without audit entries' -ForEach @( + @{ Type = 'file' } + @{ Type = 'folder' } + ) { + $item = New-TestSandboxItem -Sandbox $sandbox -Name 'Descriptor' -Directory:($Type -eq 'folder') + + $byPath = Get-NTFSInheritance -Path $item + $bySecurityDescriptor = Get-NTFSInheritance -SecurityDescriptor (Get-NTFSSecurityDescriptor -Path $item) + + $bySecurityDescriptor.AccessInheritanceEnabled | Should -Be $byPath.AccessInheritanceEnabled + $bySecurityDescriptor.AuditInheritanceEnabled | Should -Be $byPath.AuditInheritanceEnabled + } + + It 'Should report the disabled audit inheritance of a as for its path' -Skip:(-not $canChangeAudit) -ForEach @( + @{ Type = 'file' } + @{ Type = 'folder' } + ) { + $item = New-TestSandboxItem -Sandbox $sandbox -Name 'Descriptor' -Directory:($Type -eq 'folder') + Disable-NTFSAuditInheritance -Path $item -ErrorAction Stop + + $byPath = Get-NTFSInheritance -Path $item + $bySecurityDescriptor = Get-NTFSInheritance -SecurityDescriptor (Get-NTFSSecurityDescriptor -Path $item) + + $byPath.AuditInheritanceEnabled | Should -BeFalse + $bySecurityDescriptor.AuditInheritanceEnabled | Should -BeFalse + $bySecurityDescriptor.AccessInheritanceEnabled | Should -Be $byPath.AccessInheritanceEnabled + } + It 'Should report the audit inheritance as $null for a security descriptor without the audit entries' { $sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList ( (Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access