Browse Source

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 <ai@example.com>
pull/119/head
Raimund Andree 2 days ago
parent
commit
962887aa6f
  1. 14
      CHANGELOG.md
  2. 8
      Security2/FileSystem/FileSystemSecurity2.cs
  3. 54
      Security2/Win32/Lib.cs
  4. 31
      Tests/Inheritance.Tests.ps1

14
CHANGELOG.md

@ -185,6 +185,20 @@ The format is based on
- Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported - Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported
`AuditInheritanceEnabled` as `$true` for a security descriptor that was `AuditInheritanceEnabled` as `$true` for a security descriptor that was
read without its audit section; it now reports `$null`, like `-Path` 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 - 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 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 pipeline, and which repeated a failed read instead of reporting the

8
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 // 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 // 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. // 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) if (HasAuditSection)
{ {
var accessSecurity = GetSecurity(item, AccessControlSections.Access); var accessSecurity = GetSecurity(item, AccessControlSections.Access);
sd.SetSecurityDescriptorBinaryForm(accessSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Access); sd.SetSecurityDescriptorBinaryForm(accessSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Access);
var auditSecurity = GetSecurity(item, AccessControlSections.Audit);
sd.SetSecurityDescriptorBinaryForm(auditSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Audit);
} }
RememberSections(); RememberSections();

54
Security2/Win32/Lib.cs

@ -177,16 +177,26 @@ namespace Security2
{ {
remoteServerAvailable = false; 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.version = AuthzRpcClientVersion.V1;
rpcInitInfo.objectUuid = AUTHZ_OBJECTUUID_WITHCAP; rpcInitInfo.objectUuid = AUTHZ_OBJECTUUID_WITHCAP;
rpcInitInfo.protocol = RCP_OVER_TCP_PROTOCOL; rpcInitInfo.protocol = RCP_OVER_TCP_PROTOCOL;
rpcInitInfo.server = serverName; 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(); int error = Marshal.GetLastWin32Error();
// The computer can't be resolved or reached (RPC server unavailable), or it doesn't offer the remote // 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; 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());
} }
} }

31
Tests/Inheritance.Tests.ps1

@ -59,6 +59,37 @@ Describe 'Get-NTFSInheritance' {
$bySecurityDescriptor.AuditInheritanceEnabled | Should -Be $byPath.AuditInheritanceEnabled $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 <Type> 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 <Type> 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' { It 'Should report the audit inheritance as $null for a security descriptor without the audit entries' {
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList ( $sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access (Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access

Loading…
Cancel
Save