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