From 3e53db04ef38a8dcaab117691b9c74b8919389bd Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:29:21 +0200 Subject: [PATCH 1/6] fix: calculate Get-FileHash2 hashes in PowerShell 7 Get-FileHash2 failed in PowerShell 7 for every algorithm, because the hash method referenced RIPEMD160, which .NET Core and later lack. RIPEMD160 and MACTripleDES are now created by name; requesting one where .NET lacks it stops the cmdlet with an error that names the algorithm and points to Windows PowerShell 5.1. MACTripleDES uses a random key, so its result differs on every call; the value is deprecated, and the cmdlet warns when it is used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 7 ++ Docs/Cmdlets/Get-FileHash2.md | 6 +- NTFSSecurity/MiscCmdlets/GetFileHash2.cs | 16 +++++ NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 8 +-- Security2/FileSystem/FileInfo/Extensions.cs | 74 ++++++++++++-------- Tests/FileHash.Tests.ps1 | 43 +++++++++--- 6 files changed, 107 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d38308..e29c247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ The format is based on ### Deprecated - Deprecate the `-PassThur` alias of `Remove-Item2`; use `-PassThru` +- Deprecate the `MACTripleDES` value of `Get-FileHash2 -Algorithm`: it uses + a random key, so its result differs on every call; the cmdlet now warns + when you use it ### Fixed @@ -149,5 +152,9 @@ The format is based on - Fix `-PassThru` of `Copy-Item2`, `Move-Item2`, and `Remove-Item2`, which wrote the item also when `-WhatIf` or a declined confirmation skipped the operation +- Fix `Get-FileHash2` in PowerShell 7, where it failed for every algorithm; + `RIPEMD160` and `MACTripleDES`, which .NET lacks there, now stop the + cmdlet with an error that names the algorithm and points to Windows + PowerShell 5.1 [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Get-FileHash2.md b/Docs/Cmdlets/Get-FileHash2.md index 783aa4d..7f3e086 100644 --- a/Docs/Cmdlets/Get-FileHash2.md +++ b/Docs/Cmdlets/Get-FileHash2.md @@ -65,7 +65,7 @@ This command groups the files below `C:\Data` by hash value and returns the grou ### -Algorithm -Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. +Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated. ```yaml Type: HashAlgorithms @@ -117,13 +117,13 @@ For every hashed file, the cmdlet writes the file object of that file, decorated ## NOTES -The cmdlet works only in Windows PowerShell. In PowerShell 7, it fails for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`, because .NET no longer includes the RIPEMD-160 implementation that the cmdlet references. In PowerShell 7, use the built-in `Get-FileHash` cmdlet instead. +In PowerShell 7, the cmdlet supports `SHA1`, `SHA256`, `SHA384`, `SHA512`, and `MD5`. .NET no longer includes `RIPEMD160` and `MACTripleDES`, so requesting one of them there stops the cmdlet with the error `HashAlgorithmNotAvailable`, which names the algorithm; use Windows PowerShell 5.1 to calculate those hashes. Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`. If the file cannot be opened because access is denied, the cmdlet takes ownership of the file with the account that runs it, calculates the hash, and restores the previous owner afterward. That fallback fails with a `GetHashError` when the account is not allowed to change the owner of the file. A file that cannot be read produces a `GetHashError` and no result. The hash is returned as an uppercase hexadecimal string without separators, which differs from the lowercase output of some other hashing tools. Compare hash values case-insensitively. -`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. +`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. The value is deprecated: the cmdlet writes a warning when you use it, and a future version will remove it. Before 5.0.0, a folder in a `-Path` array stopped the processing of that array, so the files that followed the folder were not hashed, and a file that could not be read got a result with the hash of the previous file. diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index 91ef656..ac3b7a7 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -11,6 +11,7 @@ namespace NTFSSecurity public class GetFileHash2 : BaseCmdlet { private HashAlgorithms algorithm = HashAlgorithms.SHA256; + private bool deprecationWarningWritten = false; [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] @@ -39,6 +40,21 @@ namespace NTFSSecurity protected override void ProcessRecord() { + try + { + Security2.FileSystem.FileInfo.Extensions.CreateHashAlgorithm(algorithm).Dispose(); + } + catch (PlatformNotSupportedException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "HashAlgorithmNotAvailable", ErrorCategory.NotImplemented, algorithm)); + } + + if (algorithm == HashAlgorithms.MACTripleDES && !deprecationWarningWritten) + { + WriteWarning("The MACTripleDES algorithm uses a random key, so its result differs on every call. The value is deprecated and will be removed in a future version."); + deprecationWarningWritten = true; + } + foreach (var path in paths) { string hash = null; diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 1646c8a..e0aff10 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -4082,7 +4082,7 @@ PS C:\> Disable-Privileges Algorithm - Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. + Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated. SHA1 @@ -4106,7 +4106,7 @@ PS C:\> Disable-Privileges Algorithm - Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. + Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated. HashAlgorithms @@ -4158,10 +4158,10 @@ PS C:\> Disable-Privileges - The cmdlet works only in Windows PowerShell. In PowerShell 7, it fails for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`, because .NET no longer includes the RIPEMD-160 implementation that the cmdlet references. In PowerShell 7, use the built-in `Get-FileHash` cmdlet instead. + In PowerShell 7, the cmdlet supports `SHA1`, `SHA256`, `SHA384`, `SHA512`, and `MD5`. .NET no longer includes `RIPEMD160` and `MACTripleDES`, so requesting one of them there stops the cmdlet with the error `HashAlgorithmNotAvailable`, which names the algorithm; use Windows PowerShell 5.1 to calculate those hashes. Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`. If the file cannot be opened because access is denied, the cmdlet takes ownership of the file with the account that runs it, calculates the hash, and restores the previous owner afterward. That fallback fails with a `GetHashError` when the account is not allowed to change the owner of the file. A file that cannot be read produces a `GetHashError` and no result. The hash is returned as an uppercase hexadecimal string without separators, which differs from the lowercase output of some other hashing tools. Compare hash values case-insensitively. - `MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. + `MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. The value is deprecated: the cmdlet writes a warning when you use it, and a future version will remove it. Before 5.0.0, a folder in a `-Path` array stopped the processing of that array, so the files that followed the folder were not hashed, and a file that could not be read got a result with the hash of the previous file. diff --git a/Security2/FileSystem/FileInfo/Extensions.cs b/Security2/FileSystem/FileInfo/Extensions.cs index 5a3483d..87f5b71 100644 --- a/Security2/FileSystem/FileInfo/Extensions.cs +++ b/Security2/FileSystem/FileInfo/Extensions.cs @@ -1,4 +1,6 @@ -using System.Text; +using System; +using System.Security.Cryptography; +using System.Text; namespace Security2.FileSystem.FileInfo { @@ -15,48 +17,60 @@ namespace Security2.FileSystem.FileInfo public static class Extensions { - public static string GetHash(this Alphaleonis.Win32.Filesystem.FileInfo file, HashAlgorithms algorithm) { byte[] hash = null; + using (var hashAlgorithm = CreateHashAlgorithm(algorithm)) using (var fileStream = file.OpenRead()) { - switch (algorithm) - { - case HashAlgorithms.MD5: - hash = System.Security.Cryptography.MD5.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.SHA1: - hash = System.Security.Cryptography.SHA1.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.SHA256: - hash = System.Security.Cryptography.SHA256.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.SHA384: - hash = System.Security.Cryptography.SHA384.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.SHA512: - hash = System.Security.Cryptography.SHA512.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.MACTripleDES: - hash = System.Security.Cryptography.MACTripleDES.Create().ComputeHash(fileStream); - break; - case HashAlgorithms.RIPEMD160: - hash = System.Security.Cryptography.RIPEMD160.Create().ComputeHash(fileStream); - break; - } - - fileStream.Close(); + hash = hashAlgorithm.ComputeHash(fileStream); } - var sb = new StringBuilder(hash.Length); + var sb = new StringBuilder(hash.Length * 2); for (var i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } - + return sb.ToString(); } + + /// + /// Creates the hash algorithm. Throws a PlatformNotSupportedException that names the algorithm when the + /// .NET runtime lacks it, as .NET Core and later lack RIPEMD160 and MACTripleDES. + /// + /// The hash algorithm to create. + /// A new instance of the hash algorithm. + public static HashAlgorithm CreateHashAlgorithm(HashAlgorithms algorithm) + { + switch (algorithm) + { + case HashAlgorithms.MD5: + return MD5.Create(); + case HashAlgorithms.SHA1: + return SHA1.Create(); + case HashAlgorithms.SHA256: + return SHA256.Create(); + case HashAlgorithms.SHA384: + return SHA384.Create(); + case HashAlgorithms.SHA512: + return SHA512.Create(); + case HashAlgorithms.MACTripleDES: + case HashAlgorithms.RIPEMD160: + // Created by name: a reference to the type would make every hash fail where the type is missing. + var hashAlgorithm = CryptoConfig.CreateFromName(algorithm.ToString()) as HashAlgorithm; + if (hashAlgorithm == null) + { + throw new PlatformNotSupportedException(string.Format( + "The hash algorithm '{0}' is not available in this version of .NET. Use Windows PowerShell 5.1 to calculate it.", + algorithm)); + } + + return hashAlgorithm; + default: + throw new ArgumentOutOfRangeException("algorithm", algorithm, "Unknown hash algorithm."); + } + } } } \ No newline at end of file diff --git a/Tests/FileHash.Tests.ps1 b/Tests/FileHash.Tests.ps1 index 91eeb2c..271eb71 100644 --- a/Tests/FileHash.Tests.ps1 +++ b/Tests/FileHash.Tests.ps1 @@ -9,6 +9,7 @@ param () BeforeDiscovery { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force $isElevated = Test-IsElevated + $isCore = $PSVersionTable.PSEdition -eq 'Core' } BeforeAll { @@ -34,12 +35,40 @@ AfterAll { } Describe 'Get-FileHash2' { + Context 'Algorithms' { + # Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm, because it referenced RIPEMD160. + It 'Should return the hash of Get-FileHash for <_>' -ForEach @('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5') { + $result = Get-FileHash2 -Path $first -Algorithm $_ + + $result.Hash | Should -BeExactly (Get-FileHash -LiteralPath $first -Algorithm $_).Hash + $result.Algorithm | Should -Be $_ + } + + It 'Should calculate RIPEMD160 in Windows PowerShell' -Skip:$isCore { + $expected = [BitConverter]::ToString( + [System.Security.Cryptography.RIPEMD160]::Create().ComputeHash([IO.File]::ReadAllBytes($first)) + ).Replace('-', '') + + (Get-FileHash2 -Path $first -Algorithm RIPEMD160).Hash | Should -BeExactly $expected + } + + It 'Should stop with an error that names <_> in PowerShell 7' -Skip:(-not $isCore) -ForEach @('RIPEMD160', 'MACTripleDES') { + $algorithm = $_ + + { Get-FileHash2 -Path $first -Algorithm $algorithm -ErrorAction Stop } | + Should -Throw -ExpectedMessage "*'$algorithm'*Windows PowerShell 5.1*" + } + + It 'Should warn that MACTripleDES is deprecated' -Skip:$isCore { + $result = Get-FileHash2 -Path $first -Algorithm MACTripleDES -WarningVariable hashWarnings -WarningAction SilentlyContinue + + $result.Hash | Should -Not -BeNullOrEmpty + $hashWarnings | Should -HaveCount 1 + $hashWarnings[0].Message | Should -BeLike '*MACTripleDES*random key*deprecated*' + } + } Context 'When -Path contains a folder' { It 'Should skip the folder and hash the files that follow it' { - if ($PSVersionTable.PSEdition -eq 'Core') { - Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160' - } - $results = @(Get-FileHash2 -Path $first, $folder, $second -ErrorVariable hashErrors -ErrorAction SilentlyContinue) $hashErrors | Should -BeNullOrEmpty @@ -51,9 +80,6 @@ Describe 'Get-FileHash2' { Context 'When a file cannot be read' { # Before 5.0.0, the cmdlet wrote a result for the file anyway, with the hash of the previous file. It 'Should write an error and no result for the file' { - if ($PSVersionTable.PSEdition -eq 'Core') { - Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160' - } $locked = New-TestSandboxItem -Sandbox $sandbox -Name 'Locked' $stream = [IO.File]::Open($locked, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) try { @@ -72,9 +98,6 @@ Describe 'Get-FileHash2' { # Before 5.0.0, the account that ran the cmdlet stayed the owner when the second attempt failed. Only an # elevated process can make another account the owner first, so the test runs in CI. It 'Should restore the previous owner' -Skip:(-not $isElevated) { - if ($PSVersionTable.PSEdition -eq 'Core') { - Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160' - } $denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied' Assert-TestSandboxPath -Sandbox $sandbox -Path $denied Set-NTFSOwner -Path $denied -Account 'S-1-5-32-544' From 4fd6c97517968f6bf1f96bfee4fd9fcd1abb99e7 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:29:44 +0200 Subject: [PATCH 2/6] feat: name the audit inheritance switches after audit entries Disable-NTFSAuditInheritance -RemoveInheritedAccessRules is now -RemoveInheritedAuditRules, and Enable-NTFSAuditInheritance -RemoveExplicitAccessRules is now -RemoveExplicitAuditRules. The old names remain aliases, so existing scripts keep working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 4 ++ Docs/Cmdlets/Disable-NTFSAuditInheritance.md | 15 +++--- Docs/Cmdlets/Enable-NTFSAuditInheritance.md | 18 +++---- .../DisableAuditInheritance.cs | 19 +++++--- .../EnableAuditInheritance.cs | 19 +++++--- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 48 +++++++++---------- Tests/Inheritance.Tests.ps1 | 27 +++++++++++ 7 files changed, 95 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e29c247..a9ecb9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,10 @@ The format is based on into the documentation, and complete the version history with the release dates from the PowerShell Gallery, the missing notes for 4.2.2, 4.2.5, and 4.2.6, and detailed notes for 4.2.4 +- Rename `-RemoveInheritedAccessRules` of `Disable-NTFSAuditInheritance` to + `-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of + `Enable-NTFSAuditInheritance` to `-RemoveExplicitAuditRules`, because they + act on audit entries; the old names still work as aliases ### Deprecated diff --git a/Docs/Cmdlets/Disable-NTFSAuditInheritance.md b/Docs/Cmdlets/Disable-NTFSAuditInheritance.md index 4fca4bc..c186a1c 100644 --- a/Docs/Cmdlets/Disable-NTFSAuditInheritance.md +++ b/Docs/Cmdlets/Disable-NTFSAuditInheritance.md @@ -15,13 +15,12 @@ Blocks the inheritance of audit rules on a file or folder. ### Path (Default) ``` -Disable-NTFSAuditInheritance [[-Path] ] [-RemoveInheritedAccessRules] [-PassThru] - [] +Disable-NTFSAuditInheritance [[-Path] ] [-RemoveInheritedAuditRules] [-PassThru] [] ``` ### SecurityDescriptor ``` -Disable-NTFSAuditInheritance [-SecurityDescriptor] [-RemoveInheritedAccessRules] +Disable-NTFSAuditInheritance [-SecurityDescriptor] [-RemoveInheritedAuditRules] [-PassThru] [] ``` @@ -29,7 +28,7 @@ Disable-NTFSAuditInheritance [-SecurityDescriptor] [-Rem The `Disable-NTFSAuditInheritance` cmdlet protects the system access control list (SACL) of a file or folder, so that the audit rules of the parent folder no longer apply to the item. From then on, only the audit rules stored in the item's own SACL decide which access attempts are written to the security event log. -By default, the audit rules that the item currently inherits are copied into its SACL before inheritance is blocked, so the auditing behavior stays the same. The `-RemoveInheritedAccessRules` switch discards the inherited audit rules instead of copying them, which leaves only the audit rules that were already explicit on the item. Despite its name, the switch acts on audit rules, not on access rules. +By default, the audit rules that the item currently inherits are copied into its SACL before inheritance is blocked, so the auditing behavior stays the same. The `-RemoveInheritedAuditRules` switch discards the inherited audit rules instead of copying them, which leaves only the audit rules that were already explicit on the item. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`; that name still works as an alias. In the `Path` parameter set the cmdlet reads the audit section of the item's security descriptor, changes it, and writes it back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. @@ -48,7 +47,7 @@ This command protects the SACL of `C:\Data\Projects` and copies the audit rules ### Example 2: Block audit inheritance and discard the inherited rules ```PowerShell -PS C:\> Disable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveInheritedAccessRules -PassThru +PS C:\> Disable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveInheritedAuditRules -PassThru ``` This command protects the SACL and removes the inherited audit rules instead of copying them, so the folder is audited only by the rules that were already explicit on it. `-PassThru` returns the resulting state, in which `AuditInheritanceEnabled` is `$false`. @@ -105,14 +104,14 @@ Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` -### -RemoveInheritedAccessRules +### -RemoveInheritedAuditRules -Indicates that the audit rules the item currently inherits are discarded. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. +Indicates that the audit rules the item currently inherits are discarded. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`, which remains an alias. ```yaml Type: SwitchParameter Parameter Sets: (All) -Aliases: +Aliases: RemoveInheritedAccessRules Required: False Position: Named diff --git a/Docs/Cmdlets/Enable-NTFSAuditInheritance.md b/Docs/Cmdlets/Enable-NTFSAuditInheritance.md index 36fd685..7c5e6d9 100644 --- a/Docs/Cmdlets/Enable-NTFSAuditInheritance.md +++ b/Docs/Cmdlets/Enable-NTFSAuditInheritance.md @@ -15,20 +15,20 @@ Restores the inheritance of audit rules on a file or folder. ### Path (Default) ``` -Enable-NTFSAuditInheritance [[-Path] ] [-PassThru] [-RemoveExplicitAccessRules] [] +Enable-NTFSAuditInheritance [[-Path] ] [-PassThru] [-RemoveExplicitAuditRules] [] ``` ### SecurityDescriptor ``` Enable-NTFSAuditInheritance [-SecurityDescriptor] [-PassThru] - [-RemoveExplicitAccessRules] [] + [-RemoveExplicitAuditRules] [] ``` ## DESCRIPTION The `Enable-NTFSAuditInheritance` cmdlet removes the protection from the system access control list (SACL) of a file or folder, so that the item inherits audit rules from its parent folder again. -By default, the audit rules that are stored directly on the item are kept, and the inherited rules are added to them. An item that was processed by `Disable-NTFSAuditInheritance` therefore ends up with the inherited audit rules twice: once as the explicit copies that were created when inheritance was blocked, and once as true inherited rules. The `-RemoveExplicitAccessRules` switch deletes every audit rule that is stored directly on the item, which leaves only the inherited ones. Despite its name, the switch acts on audit rules, not on access rules. +By default, the audit rules that are stored directly on the item are kept, and the inherited rules are added to them. An item that was processed by `Disable-NTFSAuditInheritance` therefore ends up with the inherited audit rules twice: once as the explicit copies that were created when inheritance was blocked, and once as true inherited rules. The `-RemoveExplicitAuditRules` switch deletes every audit rule that is stored directly on the item, which leaves only the inherited ones. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`; that name still works as an alias. In the `Path` parameter set the cmdlet reads the audit section of the item's security descriptor, changes it, and writes it back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. @@ -47,7 +47,7 @@ This command lets `C:\Data\Projects` inherit the audit rules of `C:\Data` again. ### Example 2: Restore audit inheritance and drop the explicit rules ```PowerShell -PS C:\> Enable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveExplicitAccessRules -PassThru +PS C:\> Enable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveExplicitAuditRules -PassThru ``` This command removes every audit rule that is stored directly on the folder and lets it inherit from `C:\Data` again, so the folder is audited exactly like its parent. `-PassThru` returns the resulting state, in which `AuditInheritanceEnabled` is `$true`. @@ -55,7 +55,7 @@ This command removes every audit rule that is stored directly on the folder and ### Example 3: Repair a whole folder tree ```PowerShell -PS C:\> Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSInheritance | Where-Object { $_.AuditInheritanceEnabled -eq $false } | Enable-NTFSAuditInheritance -RemoveExplicitAccessRules +PS C:\> Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSInheritance | Where-Object { $_.AuditInheritanceEnabled -eq $false } | Enable-NTFSAuditInheritance -RemoveExplicitAuditRules ``` This command finds every item below `C:\Data` whose audit inheritance is blocked and restores it. The comparison with `$false` is deliberate: `AuditInheritanceEnabled` is `$null` for items whose audit section could not be read, and those items are skipped instead of being processed. @@ -64,7 +64,7 @@ This command finds every item below `C:\Data` whose audit inheritance is blocked ```PowerShell PS C:\> $sd = Get-NTFSSecurityDescriptor -Path C:\Data\Projects -PS C:\> Enable-NTFSAuditInheritance -SecurityDescriptor $sd -RemoveExplicitAccessRules +PS C:\> Enable-NTFSAuditInheritance -SecurityDescriptor $sd -RemoveExplicitAuditRules PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd ``` @@ -104,14 +104,14 @@ Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` -### -RemoveExplicitAccessRules +### -RemoveExplicitAuditRules -Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. +Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`, which remains an alias. ```yaml Type: SwitchParameter Parameter Sets: (All) -Aliases: +Aliases: RemoveExplicitAccessRules Required: False Position: Named diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs index c64168e..610acb3 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs @@ -10,7 +10,7 @@ namespace NTFSSecurity [OutputType(typeof(FileSystemInheritanceInfo))] public class DisableAuditInheritance : BaseCmdletWithPrivControl { - private bool removeInheritedAccessRules; + private bool removeInheritedAuditRules; private bool passThru; [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] @@ -38,11 +38,16 @@ namespace NTFSSecurity } } + /// + /// Removes the inherited audit entries instead of copying them to the item. Before 5.0.0, the switch was + /// named RemoveInheritedAccessRules, which remains an alias. + /// [Parameter] - public SwitchParameter RemoveInheritedAccessRules + [Alias("RemoveInheritedAccessRules")] + public SwitchParameter RemoveInheritedAuditRules { - get { return removeInheritedAccessRules; } - set { removeInheritedAccessRules = value; } + get { return removeInheritedAuditRules; } + set { removeInheritedAuditRules = value; } } [Parameter] @@ -77,7 +82,7 @@ namespace NTFSSecurity try { - FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAccessRules); + FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAuditRules); } catch (UnauthorizedAccessException) { @@ -85,7 +90,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAccessRules); + FileSystemInheritanceInfo.DisableAuditInheritance(item, removeInheritedAuditRules); }); } catch (Exception ex2) @@ -111,7 +116,7 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemInheritanceInfo.DisableAuditInheritance(sd, removeInheritedAccessRules); + FileSystemInheritanceInfo.DisableAuditInheritance(sd, removeInheritedAuditRules); if (passThru) { diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs index 5a19158..a5f23cc 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs @@ -9,7 +9,7 @@ namespace NTFSSecurity [OutputType(typeof(FileSystemInheritanceInfo))] public class EnableAuditInheritance : BaseCmdletWithPrivControl { - private bool removeExplicitAccessRules; + private bool removeExplicitAuditRules; private bool passThru; [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] @@ -44,11 +44,16 @@ namespace NTFSSecurity set { passThru = value; } } + /// + /// Removes the explicit audit entries of the item. Before 5.0.0, the switch was named + /// RemoveExplicitAccessRules, which remains an alias. + /// [Parameter] - public SwitchParameter RemoveExplicitAccessRules + [Alias("RemoveExplicitAccessRules")] + public SwitchParameter RemoveExplicitAuditRules { - get { return removeExplicitAccessRules; } - set { removeExplicitAccessRules = value; } + get { return removeExplicitAuditRules; } + set { removeExplicitAuditRules = value; } } protected override void BeginProcessing() @@ -76,7 +81,7 @@ namespace NTFSSecurity try { - FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAccessRules); + FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAuditRules); } catch (UnauthorizedAccessException) { @@ -84,7 +89,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAccessRules); + FileSystemInheritanceInfo.EnableAuditInheritance(item, removeExplicitAuditRules); }); } catch (Exception ex2) @@ -110,7 +115,7 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemInheritanceInfo.EnableAuditInheritance(sd, removeExplicitAccessRules); + FileSystemInheritanceInfo.EnableAuditInheritance(sd, removeExplicitAuditRules); if (passThru) { diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index e0aff10..185672a 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -2499,7 +2499,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The `Disable-NTFSAuditInheritance` cmdlet protects the system access control list (SACL) of a file or folder, so that the audit rules of the parent folder no longer apply to the item. From then on, only the audit rules stored in the item's own SACL decide which access attempts are written to the security event log. - By default, the audit rules that the item currently inherits are copied into its SACL before inheritance is blocked, so the auditing behavior stays the same. The `-RemoveInheritedAccessRules` switch discards the inherited audit rules instead of copying them, which leaves only the audit rules that were already explicit on the item. Despite its name, the switch acts on audit rules, not on access rules. + By default, the audit rules that the item currently inherits are copied into its SACL before inheritance is blocked, so the auditing behavior stays the same. The `-RemoveInheritedAuditRules` switch discards the inherited audit rules instead of copying them, which leaves only the audit rules that were already explicit on the item. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`; that name still works as an alias. In the `Path` parameter set the cmdlet reads the audit section of the item's security descriptor, changes it, and writes it back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. Reading and writing the audit section requires the Security privilege, so run this cmdlet in an elevated session. `-Path` accepts pipeline input by value and by property name through its `FullName` alias, so the output of `Get-ChildItem`, `Get-ChildItem2`, `Get-Item2`, and `Get-NTFSInheritance` binds to it. The cmdlet affects only the audit rules; use `Disable-NTFSAccessInheritance` for the access rules. @@ -2529,10 +2529,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - - RemoveInheritedAccessRules + + RemoveInheritedAuditRules - Indicates that the audit rules the item currently inherits are discarded. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. + Indicates that the audit rules the item currently inherits are discarded. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`, which remains an alias. SwitchParameter @@ -2568,10 +2568,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - - RemoveInheritedAccessRules + + RemoveInheritedAuditRules - Indicates that the audit rules the item currently inherits are discarded. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. + Indicates that the audit rules the item currently inherits are discarded. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`, which remains an alias. SwitchParameter @@ -2606,10 +2606,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd None - - RemoveInheritedAccessRules + + RemoveInheritedAuditRules - Indicates that the audit rules the item currently inherits are discarded. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. + Indicates that the audit rules the item currently inherits are discarded. By default, when the switch is omitted, the inherited audit rules are copied into the item's own SACL as explicit rules and auditing continues unchanged. Before 5.0.0, the switch was named `-RemoveInheritedAccessRules`, which remains an alias. SwitchParameter @@ -2681,7 +2681,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Example 2: Block audit inheritance and discard the inherited rules - PS C:\> Disable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveInheritedAccessRules -PassThru + PS C:\> Disable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveInheritedAuditRules -PassThru This command protects the SACL and removes the inherited audit rules instead of copying them, so the folder is audited only by the rules that were already explicit on it. `-PassThru` returns the resulting state, in which `AuditInheritanceEnabled` is `$false`. @@ -3116,7 +3116,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The `Enable-NTFSAuditInheritance` cmdlet removes the protection from the system access control list (SACL) of a file or folder, so that the item inherits audit rules from its parent folder again. - By default, the audit rules that are stored directly on the item are kept, and the inherited rules are added to them. An item that was processed by `Disable-NTFSAuditInheritance` therefore ends up with the inherited audit rules twice: once as the explicit copies that were created when inheritance was blocked, and once as true inherited rules. The `-RemoveExplicitAccessRules` switch deletes every audit rule that is stored directly on the item, which leaves only the inherited ones. Despite its name, the switch acts on audit rules, not on access rules. + By default, the audit rules that are stored directly on the item are kept, and the inherited rules are added to them. An item that was processed by `Disable-NTFSAuditInheritance` therefore ends up with the inherited audit rules twice: once as the explicit copies that were created when inheritance was blocked, and once as true inherited rules. The `-RemoveExplicitAuditRules` switch deletes every audit rule that is stored directly on the item, which leaves only the inherited ones. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`; that name still works as an alias. In the `Path` parameter set the cmdlet reads the audit section of the item's security descriptor, changes it, and writes it back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. Reading and writing the audit section requires the Security privilege, so run this cmdlet in an elevated session. `-Path` accepts pipeline input by value and by property name through its `FullName` alias, so the output of `Get-ChildItem`, `Get-ChildItem2`, `Get-Item2`, and `Get-NTFSInheritance` binds to it. The cmdlet affects only the audit rules; use `Enable-NTFSAccessInheritance` for the access rules. @@ -3146,10 +3146,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - - RemoveExplicitAccessRules + + RemoveExplicitAuditRules - Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. + Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`, which remains an alias. SwitchParameter @@ -3185,10 +3185,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - - RemoveExplicitAccessRules + + RemoveExplicitAuditRules - Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. + Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`, which remains an alias. SwitchParameter @@ -3223,10 +3223,10 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd None - - RemoveExplicitAccessRules + + RemoveExplicitAuditRules - Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. Despite its name, the switch acts on the audit rules in the SACL, not on access rules. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. + Indicates that every audit rule stored directly on the item is removed when inheritance is restored, so that the item ends up with the inherited audit rules only. By default, when the switch is omitted, the explicit audit rules are kept and the inherited rules are added to them, which usually duplicates the rules that `Disable-NTFSAuditInheritance` copied earlier. Before 5.0.0, the switch was named `-RemoveExplicitAccessRules`, which remains an alias. SwitchParameter @@ -3298,14 +3298,14 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Example 2: Restore audit inheritance and drop the explicit rules - PS C:\> Enable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveExplicitAccessRules -PassThru + PS C:\> Enable-NTFSAuditInheritance -Path C:\Data\Projects -RemoveExplicitAuditRules -PassThru This command removes every audit rule that is stored directly on the folder and lets it inherit from `C:\Data` again, so the folder is audited exactly like its parent. `-PassThru` returns the resulting state, in which `AuditInheritanceEnabled` is `$true`. ------------ Example 3: Repair a whole folder tree ------------ - PS C:\> Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSInheritance | Where-Object { $_.AuditInheritanceEnabled -eq $false } | Enable-NTFSAuditInheritance -RemoveExplicitAccessRules + PS C:\> Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSInheritance | Where-Object { $_.AuditInheritanceEnabled -eq $false } | Enable-NTFSAuditInheritance -RemoveExplicitAuditRules This command finds every item below `C:\Data` whose audit inheritance is blocked and restores it. The comparison with `$false` is deliberate: `AuditInheritanceEnabled` is `$null` for items whose audit section could not be read, and those items are skipped instead of being processed. @@ -3313,7 +3313,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd ------ Example 4: Change a security descriptor in memory ------ PS C:\> $sd = Get-NTFSSecurityDescriptor -Path C:\Data\Projects -PS C:\> Enable-NTFSAuditInheritance -SecurityDescriptor $sd -RemoveExplicitAccessRules +PS C:\> Enable-NTFSAuditInheritance -SecurityDescriptor $sd -RemoveExplicitAuditRules PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The first two commands read the security descriptor and restore audit inheritance in memory, which does not change anything on disk. The third command writes the descriptor back and applies the change. diff --git a/Tests/Inheritance.Tests.ps1 b/Tests/Inheritance.Tests.ps1 index 80d5fc3..b3a1976 100644 --- a/Tests/Inheritance.Tests.ps1 +++ b/Tests/Inheritance.Tests.ps1 @@ -149,3 +149,30 @@ Describe 'Set-NTFSInheritance' { } } } + +Describe 'Audit inheritance switches' { + # Before 5.0.0, the switches were named after access entries, although they remove audit entries. + It ' should take - with the alias -' -ForEach @( + @{ Command = 'Disable-NTFSAuditInheritance'; Name = 'RemoveInheritedAuditRules'; Alias = 'RemoveInheritedAccessRules' } + @{ Command = 'Enable-NTFSAuditInheritance'; Name = 'RemoveExplicitAuditRules'; Alias = 'RemoveExplicitAccessRules' } + ) { + $parameter = (Get-Command -Name $Command).Parameters[$Name] + + $parameter | Should -Not -BeNullOrEmpty + $parameter.SwitchParameter | Should -BeTrue + $parameter.Aliases | Should -Contain $Alias + } + + It ' should bind -' -ForEach @( + @{ Command = 'Disable-NTFSAuditInheritance'; Switch = 'RemoveInheritedAuditRules' } + @{ Command = 'Disable-NTFSAuditInheritance'; Switch = 'RemoveInheritedAccessRules' } + @{ Command = 'Enable-NTFSAuditInheritance'; Switch = 'RemoveExplicitAuditRules' } + @{ Command = 'Enable-NTFSAuditInheritance'; Switch = 'RemoveExplicitAccessRules' } + ) { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditSwitch' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + $parameters = @{ Path = $file; $Switch = $true } + + { & $Command @parameters -ErrorAction SilentlyContinue } | Should -Not -Throw + } +} \ No newline at end of file From 437d353c8f43f780e713760e1eb63289374e8c6e Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:30:06 +0200 Subject: [PATCH 3/6] feat!: keep entries in Set-NTFSInheritance like the dedicated cmdlets -AccessInheritanceEnabled $false now copies the inherited access entries into the DACL, and -AuditInheritanceEnabled $true keeps the explicit audit entries, as Disable-NTFSAccessInheritance and Enable-NTFSAuditInheritance do without their switches (Decision 13). BREAKING CHANGE: to remove the entries, use Disable-NTFSAccessInheritance -RemoveInheritedAccessRules or Enable-NTFSAuditInheritance -RemoveExplicitAuditRules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .../0013-set-inheritance-keeps-entries.md | 34 ++++++++++++++++ .memory-bank/systemPatterns.md | 1 + CHANGELOG.md | 6 +++ Docs/Cmdlets/Set-NTFSInheritance.md | 10 ++--- Docs/Concepts.md | 10 +++-- .../InheritanceCmdlets/SetInheritance.cs | 8 ++-- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 18 ++++----- Tests/Inheritance.Tests.ps1 | 39 +++++++++++++++++++ 8 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 .memory-bank/decisions/0013-set-inheritance-keeps-entries.md diff --git a/.memory-bank/decisions/0013-set-inheritance-keeps-entries.md b/.memory-bank/decisions/0013-set-inheritance-keeps-entries.md new file mode 100644 index 0000000..12c768b --- /dev/null +++ b/.memory-bank/decisions/0013-set-inheritance-keeps-entries.md @@ -0,0 +1,34 @@ +--- +status: accepted +date: 2026-10-05 +last-verified: 2026-10-05 +owner: shared +source: maintainer decision D3 for the overnight run of 2026-10-04 (defect group E) +--- + +# Decision 13: Set-NTFSInheritance keeps entries like the dedicated cmdlets + +- Choice: `Set-NTFSInheritance` uses the defaults of the dedicated cmdlets. + `-AccessInheritanceEnabled $false` copies the inherited access entries + into the DACL, like `Disable-NTFSAccessInheritance`, and + `-AuditInheritanceEnabled $true` keeps the explicit audit entries, like + `Enable-NTFSAuditInheritance`. The other two directions already matched. + The same applies to a security descriptor in memory, whose kept entries + stay marked as inherited until it is written. +- Way back: `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` + removes the inherited access entries, and + `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules` removes the + explicit audit entries. `Set-NTFSInheritance` gets no switches for that. +- Rationale: Before 5.0.0, two of the four directions removed entries, + unlike the dedicated cmdlets. Turning access inheritance off on an item + without explicit entries left an empty DACL, which denies access to + everyone. 5.0.0 is a major version, so the change ships there, listed + under `Changed` in the changelog. +- Related: `Clear-NTFSAccess -DisableInheritance` keeps discarding the + inherited entries, because removing every entry is the purpose of that + cmdlet; its page states the empty DACL and its risk (decision D2 of the + same run). The audit switches were renamed to `-RemoveInheritedAuditRules` + and `-RemoveExplicitAuditRules`, with the old names as aliases (D4). +- Rejected: adding `-RemoveInheritedAccessRules` and + `-RemoveExplicitAuditRules` switches to `Set-NTFSInheritance`, which would + duplicate the dedicated cmdlets. diff --git a/.memory-bank/systemPatterns.md b/.memory-bank/systemPatterns.md index 35d657d..83394d7 100644 --- a/.memory-bank/systemPatterns.md +++ b/.memory-bank/systemPatterns.md @@ -58,6 +58,7 @@ Each Decision record is a file in `decisions/`; read only the relevant ones. | 10 | [One version for the manifest, assemblies, and changelog](decisions/0010-one-version.md) | | 11 | [CI and the wiki run on GitHub Actions](decisions/0011-github-actions.md) | | 12 | [Releases are built and published by CI on a version tag](decisions/0012-ci-releases.md) | +| 13 | [Set-NTFSInheritance keeps entries like the dedicated cmdlets](decisions/0013-set-inheritance-keeps-entries.md) | ## Patterns diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ecb9a..ba8dd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,12 @@ The format is based on `-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of `Enable-NTFSAuditInheritance` to `-RemoveExplicitAuditRules`, because they act on audit entries; the old names still work as aliases +- `Set-NTFSInheritance` keeps entries like the dedicated cmdlets: + `-AccessInheritanceEnabled $false` now copies the inherited access entries + into the DACL instead of removing them, and + `-AuditInheritanceEnabled $true` now keeps the explicit audit entries. To + remove them, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` + or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules` ### Deprecated diff --git a/Docs/Cmdlets/Set-NTFSInheritance.md b/Docs/Cmdlets/Set-NTFSInheritance.md index be5a2d4..855d9c2 100644 --- a/Docs/Cmdlets/Set-NTFSInheritance.md +++ b/Docs/Cmdlets/Set-NTFSInheritance.md @@ -29,7 +29,7 @@ Set-NTFSInheritance [-SecurityDescriptor] [-AccessInheri The `Set-NTFSInheritance` cmdlet turns the inheritance of access rules and audit rules on or off in a single call. It reads the current state of the item first and changes a section only when the requested value differs from the current one, which makes the cmdlet suitable for repeatedly applying a desired state to a folder tree. -The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches and it does not use their defaults. `-AccessInheritanceEnabled $false` discards the inherited access rules instead of copying them into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` removes the explicit audit rules. Use the individual Enable and Disable cmdlets when you need the opposite behavior. +The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. Omit `-AccessInheritanceEnabled` or `-AuditInheritanceEnabled` to leave that section unchanged. Changing the audit section requires the Security privilege and therefore an elevated session. @@ -43,7 +43,7 @@ In the `Path` parameter set the cmdlet writes each changed section back to disk PS C:\> Set-NTFSInheritance -Path C:\Data\Projects -AccessInheritanceEnabled $false -AuditInheritanceEnabled $false ``` -This command protects the DACL and the SACL of `C:\Data\Projects`. The inherited access rules are discarded, so make sure the folder has explicit access rules of its own; the inherited audit rules are copied into the folder's SACL. Changing the audit section requires an elevated session. +This command protects the DACL and the SACL of `C:\Data\Projects`. The inherited access and audit rules are copied into the folder's DACL and SACL, so the effective permissions and the auditing stay the same. Changing the audit section requires an elevated session. ### Example 2: Restore inheritance of both sections @@ -51,7 +51,7 @@ This command protects the DACL and the SACL of `C:\Data\Projects`. The inherited PS C:\> Set-NTFSInheritance -Path C:\Data\Projects -AccessInheritanceEnabled $true -AuditInheritanceEnabled $true -PassThru ``` -This command lets the folder inherit from `C:\Data` again. The explicit access rules are kept, the explicit audit rules are removed, and `-PassThru` returns the resulting state. +This command lets the folder inherit from `C:\Data` again. The explicit access and audit rules are kept, and `-PassThru` returns the resulting state. ### Example 3: Save a state and apply it again @@ -77,7 +77,7 @@ The first two commands read the security descriptor and change its inheritance i ### -AccessInheritanceEnabled -Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and discards the rules the item currently inherits, which leaves only its explicit rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. +Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and copies the rules the item currently inherits into it, so the effective permissions stay the same. Before 5.0.0, `$false` discarded the inherited rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. ```yaml Type: Boolean @@ -93,7 +93,7 @@ Accept wildcard characters: False ### -AuditInheritanceEnabled -Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and removes the audit rules that are stored directly on the item; `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. +Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and keeps the audit rules that are stored directly on the item (before 5.0.0, it removed them); `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. ```yaml Type: Boolean diff --git a/Docs/Concepts.md b/Docs/Concepts.md index 18f347c..1852861 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -126,11 +126,13 @@ entries: - `Enable-NTFSAccessInheritance` restores inheritance and keeps the explicit entries unless you use `-RemoveExplicitAccessRules`. - `Get-NTFSInheritance` and `Set-NTFSInheritance` read and set both - settings at once. Unlike the dedicated cmdlets, `Set-NTFSInheritance` - removes the inherited access entries when it turns access inheritance off, - and removes the explicit audit entries when it turns audit inheritance on. + settings at once. Like the dedicated cmdlets without their switches, + `Set-NTFSInheritance` keeps the entries; before 5.0.0, it removed the + inherited access entries when it turned access inheritance off, and the + explicit audit entries when it turned audit inheritance on. The audit equivalents of the dedicated cmdlets are - `Disable-NTFSAuditInheritance` and `Enable-NTFSAuditInheritance`. + `Disable-NTFSAuditInheritance` and `Enable-NTFSAuditInheritance`, with + the switches `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`. ### The AppliesTo parameter diff --git a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs index ec3333b..ecc4459 100644 --- a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs @@ -142,7 +142,7 @@ namespace NTFSSecurity else { WriteVerbose("Calling DisableAccessInheritance"); - FileSystemInheritanceInfo.DisableAccessInheritance(item, true); + FileSystemInheritanceInfo.DisableAccessInheritance(item, false); } } @@ -151,7 +151,7 @@ namespace NTFSSecurity if (auditInheritanceEnabled.Value) { WriteVerbose("Calling EnableAuditInheritance"); - FileSystemInheritanceInfo.EnableAuditInheritance(item, true); + FileSystemInheritanceInfo.EnableAuditInheritance(item, false); } else { @@ -175,7 +175,7 @@ namespace NTFSSecurity else { WriteVerbose("Calling DisableAccessInheritance"); - FileSystemInheritanceInfo.DisableAccessInheritance(sd, true); + FileSystemInheritanceInfo.DisableAccessInheritance(sd, false); } } @@ -184,7 +184,7 @@ namespace NTFSSecurity if (auditInheritanceEnabled.Value) { WriteVerbose("Calling EnableAuditInheritance"); - FileSystemInheritanceInfo.EnableAuditInheritance(sd, true); + FileSystemInheritanceInfo.EnableAuditInheritance(sd, false); } else { diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 185672a..cea22cd 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -9334,7 +9334,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The `Set-NTFSInheritance` cmdlet turns the inheritance of access rules and audit rules on or off in a single call. It reads the current state of the item first and changes a section only when the requested value differs from the current one, which makes the cmdlet suitable for repeatedly applying a desired state to a folder tree. - The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches and it does not use their defaults. `-AccessInheritanceEnabled $false` discards the inherited access rules instead of copying them into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` removes the explicit audit rules. Use the individual Enable and Disable cmdlets when you need the opposite behavior. + The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. Omit `-AccessInheritanceEnabled` or `-AuditInheritanceEnabled` to leave that section unchanged. Changing the audit section requires the Security privilege and therefore an elevated session. In the `Path` parameter set the cmdlet writes each changed section back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. `-Path`, `-AccessInheritanceEnabled`, and `-AuditInheritanceEnabled` all accept pipeline input by property name, so a `Security2.FileSystemInheritanceInfo` object from `Get-NTFSInheritance` binds to all three at once. @@ -9356,7 +9356,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AccessInheritanceEnabled - Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and discards the rules the item currently inherits, which leaves only its explicit rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. + Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and copies the rules the item currently inherits into it, so the effective permissions stay the same. Before 5.0.0, `$false` discarded the inherited rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. Boolean @@ -9368,7 +9368,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AuditInheritanceEnabled - Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and removes the audit rules that are stored directly on the item; `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. + Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and keeps the audit rules that are stored directly on the item (before 5.0.0, it removed them); `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. Boolean @@ -9408,7 +9408,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AccessInheritanceEnabled - Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and discards the rules the item currently inherits, which leaves only its explicit rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. + Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and copies the rules the item currently inherits into it, so the effective permissions stay the same. Before 5.0.0, `$false` discarded the inherited rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. Boolean @@ -9420,7 +9420,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AuditInheritanceEnabled - Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and removes the audit rules that are stored directly on the item; `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. + Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and keeps the audit rules that are stored directly on the item (before 5.0.0, it removed them); `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. Boolean @@ -9446,7 +9446,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AccessInheritanceEnabled - Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and discards the rules the item currently inherits, which leaves only its explicit rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. + Specifies whether the item inherits access rules from its parent folder. `$true` removes the protection from the DACL and keeps the access rules that are stored directly on the item; `$false` protects the DACL and copies the rules the item currently inherits into it, so the effective permissions stay the same. Before 5.0.0, `$false` discarded the inherited rules. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the access section is left unchanged. Boolean @@ -9458,7 +9458,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd AuditInheritanceEnabled - Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and removes the audit rules that are stored directly on the item; `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. + Specifies whether the item inherits audit rules from its parent folder. `$true` removes the protection from the SACL and keeps the audit rules that are stored directly on the item (before 5.0.0, it removed them); `$false` protects the SACL and copies the inherited audit rules into it. The section is left untouched when the requested value already matches the current state. When you omit the parameter, the audit section is left unchanged. Reading and writing the audit section requires the Security privilege and therefore an elevated session. Boolean @@ -9558,14 +9558,14 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -------- Example 1: Block inheritance of both sections -------- PS C:\> Set-NTFSInheritance -Path C:\Data\Projects -AccessInheritanceEnabled $false -AuditInheritanceEnabled $false - This command protects the DACL and the SACL of `C:\Data\Projects`. The inherited access rules are discarded, so make sure the folder has explicit access rules of its own; the inherited audit rules are copied into the folder's SACL. Changing the audit section requires an elevated session. + This command protects the DACL and the SACL of `C:\Data\Projects`. The inherited access and audit rules are copied into the folder's DACL and SACL, so the effective permissions and the auditing stay the same. Changing the audit section requires an elevated session. ------- Example 2: Restore inheritance of both sections ------- PS C:\> Set-NTFSInheritance -Path C:\Data\Projects -AccessInheritanceEnabled $true -AuditInheritanceEnabled $true -PassThru - This command lets the folder inherit from `C:\Data` again. The explicit access rules are kept, the explicit audit rules are removed, and `-PassThru` returns the resulting state. + This command lets the folder inherit from `C:\Data` again. The explicit access and audit rules are kept, and `-PassThru` returns the resulting state. diff --git a/Tests/Inheritance.Tests.ps1 b/Tests/Inheritance.Tests.ps1 index b3a1976..b0fbc7b 100644 --- a/Tests/Inheritance.Tests.ps1 +++ b/Tests/Inheritance.Tests.ps1 @@ -108,6 +108,45 @@ Describe 'Inheritance cmdlets with -PassThru' { } Describe 'Set-NTFSInheritance' { + Context 'When it changes the inheritance' { + # Before 5.0.0, -AccessInheritanceEnabled $false removed the inherited access entries and + # -AuditInheritanceEnabled $true removed the explicit audit entries, unlike the dedicated cmdlets. + It 'Should keep the inherited access entries as explicit ones when it disables access inheritance' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepAccess' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + $inheritedCount = @((Get-Acl -LiteralPath $file).Access | Where-Object -Property IsInherited).Count + + Set-NTFSInheritance -Path $file -AccessInheritanceEnabled $false + + $acl = Get-Acl -LiteralPath $file + $acl.AreAccessRulesProtected | Should -BeTrue + @($acl.Access | Where-Object -Property IsInherited -EQ -Value $false) | Should -HaveCount $inheritedCount + } + + # In memory, the kept entries stay marked as inherited; Windows stores them as explicit ones on write. + It 'Should keep the inherited access entries of a security descriptor' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepDescriptor' + $sd = Get-NTFSSecurityDescriptor -Path $file + $sidType = [System.Security.Principal.SecurityIdentifier] + $inheritedCount = @($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)).Count + + Set-NTFSInheritance -SecurityDescriptor $sd -AccessInheritanceEnabled $false + + $sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue + @($sd.SecurityDescriptor.GetAccessRules($true, $true, $sidType)) | Should -HaveCount $inheritedCount + } + + It 'Should keep the explicit audit entries when it enables audit inheritance' -Skip:(-not $canChangeAudit) { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepAudit' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights Delete -AuditFlags Failure + Disable-NTFSAuditInheritance -Path $file + + Set-NTFSInheritance -Path $file -AuditInheritanceEnabled $true + + @(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1 + } + } Context 'When -AccessInheritanceEnabled or -AuditInheritanceEnabled is omitted' { BeforeEach { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'File' From 129883a197f32e251be9d6d8b9755a2ffd39c417 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:30:28 +0200 Subject: [PATCH 4/6] docs: state the empty DACL that Clear-NTFSAccess -DisableInheritance leaves The behavior stays: the cmdlet removes the explicit entries and disables inheritance without copying the inherited ones. The parameter text now states the empty DACL and its risk, and a test pins the behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Clear-NTFSAccess.md | 2 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 6 +++--- Tests/Access.Tests.ps1 | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Docs/Cmdlets/Clear-NTFSAccess.md b/Docs/Cmdlets/Clear-NTFSAccess.md index 7c2278b..62ff672 100644 --- a/Docs/Cmdlets/Clear-NTFSAccess.md +++ b/Docs/Cmdlets/Clear-NTFSAccess.md @@ -72,7 +72,7 @@ These commands replace the complete ACL of `C:\Data` in one write. The security ### -DisableInheritance -Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. Without this switch the inherited entries remain in effect. +Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. The item is left with an empty DACL, which denies access to everyone; only its owner can still change the permissions. Without this switch the inherited entries remain in effect. ```yaml Type: SwitchParameter diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index cea22cd..781722d 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -1601,7 +1601,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd DisableInheritance - Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. Without this switch the inherited entries remain in effect. + Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. The item is left with an empty DACL, which denies access to everyone; only its owner can still change the permissions. Without this switch the inherited entries remain in effect. SwitchParameter @@ -1628,7 +1628,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd DisableInheritance - Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. Without this switch the inherited entries remain in effect. + Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. The item is left with an empty DACL, which denies access to everyone; only its owner can still change the permissions. Without this switch the inherited entries remain in effect. SwitchParameter @@ -1642,7 +1642,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd DisableInheritance - Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. Without this switch the inherited entries remain in effect. + Indicates that inheritance is disabled after the explicit entries are removed, and that the inherited entries are discarded rather than copied into the item. The item is left with an empty DACL, which denies access to everyone; only its owner can still change the permissions. Without this switch the inherited entries remain in effect. SwitchParameter diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 49a67bc..b423e0f 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -285,3 +285,20 @@ Describe 'Security descriptor parameter sets' { $rule.InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None) } } + +Describe 'Clear-NTFSAccess' { + Context 'With -DisableInheritance' { + # The cmdlet does not copy the inherited entries, so the item is left with an empty DACL. This is the + # documented behavior; Set-NTFSInheritance and Disable-NTFSAccessInheritance keep the entries. + It 'Should leave the item with an empty, protected DACL' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAll' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + + Clear-NTFSAccess -Path $file -DisableInheritance + + $acl = Get-Acl -LiteralPath $file + $acl.AreAccessRulesProtected | Should -BeTrue + $acl.Access | Should -BeNullOrEmpty + } + } +} \ No newline at end of file From 4456d0a9cf8ea8c88504514926b404b88f33939f Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:35:36 +0200 Subject: [PATCH 5/6] chore(memory-bank): record the group E decisions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/activeContext.md | 7 +++++-- .memory-bank/progress.md | 27 +++++++++++---------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index 609b3fd..b8c6f8b 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -57,7 +57,10 @@ the PRs, and tags `5.0.0-rc2` after the merges. the `*-Item2` cmdlets: Windows PowerShell 379 passed, 23 skipped; PowerShell 7 348 passed, 54 skipped (402 tests). +- `ai/decisions-e` implements D2 to D5 (Decision 13): Windows PowerShell + 395 passed, 26 skipped; PowerShell 7 366 passed, 55 skipped (421 + tests). `Get-FileHash2` tests now run in PowerShell 7 as well. + ## Next step -The E decisions on `ai/decisions-e` (Decision 13 for `Set-NTFSInheritance`), -then the bugs from the issue triage (`ai/issue-fixes`) and 5.0.0-rc2. +The bugs from the issue triage (`ai/issue-fixes`), then 5.0.0-rc2. diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index c3a08e7..dc0b932 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -166,19 +166,14 @@ Numbered as agreed with the maintainer; each is documented on its page. - Found with group D: `-PassThru` of the `*-Item2` cmdlets wrote the item also when `-WhatIf` skipped the operation. -#### E: Maintainer decisions before changing behavior - -- `Clear-NTFSAccess -DisableInheritance` removes the explicit entries and - then disables inheritance without copying the inherited ones, which - leaves an empty DACL: keep that, or copy them? -- `Set-NTFSInheritance` differs from the dedicated cmdlets in two of four - directions: `-AccessInheritanceEnabled $false` removes the inherited - access entries, and `-AuditInheritanceEnabled $true` removes the - explicit audit entries; the dedicated cmdlets keep them unless a switch - is given. Align? -- The audit inheritance switches are named `*AccessRules`: add - `*AuditRules` aliases? -- `Get-FileHash2` fails in PowerShell 7 (`RIPEMD160`): drop the algorithm - there, load it lazily, or deprecate the cmdlet? To verify: - `MACTripleDES.Create()` may use a random key, so its result would differ - on every call. +#### E: Maintainer decisions (implemented on `ai/decisions-e`, not merged) + +- D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL; + the page states the result and the risk, and a test pins it. +- D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets + (Decision 13; listed under `Changed`). +- D4: `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`, with + the `*AccessRules` names as aliases. +- D5: `Get-FileHash2` works in PowerShell 7; `RIPEMD160` and + `MACTripleDES` stop it there with `HashAlgorithmNotAvailable`. + `MACTripleDES` uses a random key (verified), so it is deprecated. From e2ea2e414ded3e25e3c3d0d656881f81b179fd8e Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Mon, 5 Oct 2026 02:52:20 +0200 Subject: [PATCH 6/6] fix: address the review of the group E decisions Mark the Set-NTFSInheritance change as breaking and warn that scripts that used it to drop the inherited access entries now leave broader access in place. Report any failure to create the hash algorithm as HashAlgorithmNotAvailable, assert that error ID, check that the MACTripleDES warning appears once, and guard the descriptor test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .memory-bank/progress.md | 2 ++ CHANGELOG.md | 15 +++++++++------ Docs/Cmdlets/Set-NTFSInheritance.md | 2 +- NTFSSecurity/MiscCmdlets/GetFileHash2.cs | 5 +++++ NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 2 +- Tests/Access.Tests.ps1 | 2 +- Tests/FileHash.Tests.ps1 | 13 ++++++++----- Tests/Inheritance.Tests.ps1 | 3 ++- Tests/OutputTypes.Tests.ps1 | 2 +- 9 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index dc0b932..5c15c67 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -177,3 +177,5 @@ Numbered as agreed with the maintainer; each is documented on its page. - D5: `Get-FileHash2` works in PowerShell 7; `RIPEMD160` and `MACTripleDES` stop it there with `HashAlgorithmNotAvailable`. `MACTripleDES` uses a random key (verified), so it is deprecated. +- Review of group E: the changelog now marks the `Set-NTFSInheritance` + change as breaking and warns that it leaves broader access in place. diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8dd57..3f4c0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,12 +40,15 @@ The format is based on `-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of `Enable-NTFSAuditInheritance` to `-RemoveExplicitAuditRules`, because they act on audit entries; the old names still work as aliases -- `Set-NTFSInheritance` keeps entries like the dedicated cmdlets: - `-AccessInheritanceEnabled $false` now copies the inherited access entries - into the DACL instead of removing them, and - `-AuditInheritanceEnabled $true` now keeps the explicit audit entries. To - remove them, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` - or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules` +- **Breaking:** `Set-NTFSInheritance` keeps entries like the dedicated + cmdlets: `-AccessInheritanceEnabled $false` now copies the inherited access + entries into the DACL instead of removing them, and + `-AuditInheritanceEnabled $true` now keeps the explicit audit entries. A + script that used `-AccessInheritanceEnabled $false` to drop the inherited + access entries now leaves them in place, which grants broader access than + before. To remove the entries, use + `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or + `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules` ### Deprecated diff --git a/Docs/Cmdlets/Set-NTFSInheritance.md b/Docs/Cmdlets/Set-NTFSInheritance.md index 855d9c2..e3667c6 100644 --- a/Docs/Cmdlets/Set-NTFSInheritance.md +++ b/Docs/Cmdlets/Set-NTFSInheritance.md @@ -29,7 +29,7 @@ Set-NTFSInheritance [-SecurityDescriptor] [-AccessInheri The `Set-NTFSInheritance` cmdlet turns the inheritance of access rules and audit rules on or off in a single call. It reads the current state of the item first and changes a section only when the requested value differs from the current one, which makes the cmdlet suitable for repeatedly applying a desired state to a folder tree. -The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. +The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. Review scripts that used `-AccessInheritanceEnabled $false` to drop the inherited access rules: they now keep them, which leaves broader access in place; `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` gives the old result. Omit `-AccessInheritanceEnabled` or `-AuditInheritanceEnabled` to leave that section unchanged. Changing the audit section requires the Security privilege and therefore an elevated session. diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index ac3b7a7..113d8f0 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -48,6 +48,11 @@ namespace NTFSSecurity { ThrowTerminatingError(new ErrorRecord(ex, "HashAlgorithmNotAvailable", ErrorCategory.NotImplemented, algorithm)); } + catch (Exception ex) + { + // For example, an algorithm that a FIPS policy doesn't allow. + ThrowTerminatingError(new ErrorRecord(ex, "HashAlgorithmNotAvailable", ErrorCategory.NotImplemented, algorithm)); + } if (algorithm == HashAlgorithms.MACTripleDES && !deprecationWarningWritten) { diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 781722d..fd81345 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -9334,7 +9334,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The `Set-NTFSInheritance` cmdlet turns the inheritance of access rules and audit rules on or off in a single call. It reads the current state of the item first and changes a section only when the requested value differs from the current one, which makes the cmdlet suitable for repeatedly applying a desired state to a folder tree. - The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. + The cmdlet performs the same operations as `Enable-NTFSAccessInheritance`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAuditInheritance`, and `Disable-NTFSAuditInheritance`, but it does not expose their switches; it uses their defaults instead. `-AccessInheritanceEnabled $false` copies the inherited access rules into the item's own DACL, `-AccessInheritanceEnabled $true` keeps the explicit access rules, `-AuditInheritanceEnabled $false` copies the inherited audit rules into the item's own SACL, and `-AuditInheritanceEnabled $true` keeps the explicit audit rules. To remove the rules instead, use `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or `Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`. Before 5.0.0, `-AccessInheritanceEnabled $false` discarded the inherited access rules, and `-AuditInheritanceEnabled $true` removed the explicit audit rules. Review scripts that used `-AccessInheritanceEnabled $false` to drop the inherited access rules: they now keep them, which leaves broader access in place; `Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` gives the old result. Omit `-AccessInheritanceEnabled` or `-AuditInheritanceEnabled` to leave that section unchanged. Changing the audit section requires the Security privilege and therefore an elevated session. In the `Path` parameter set the cmdlet writes each changed section back to disk immediately. In the `SecurityDescriptor` parameter set it changes the `Security2.FileSystemSecurity2` object in memory only; nothing reaches the file system until you pass that object to `Set-NTFSSecurityDescriptor`. `-Path`, `-AccessInheritanceEnabled`, and `-AuditInheritanceEnabled` all accept pipeline input by property name, so a `Security2.FileSystemInheritanceInfo` object from `Get-NTFSInheritance` binds to all three at once. diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index b423e0f..5582a81 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -301,4 +301,4 @@ Describe 'Clear-NTFSAccess' { $acl.Access | Should -BeNullOrEmpty } } -} \ No newline at end of file +} diff --git a/Tests/FileHash.Tests.ps1 b/Tests/FileHash.Tests.ps1 index 271eb71..221c7b8 100644 --- a/Tests/FileHash.Tests.ps1 +++ b/Tests/FileHash.Tests.ps1 @@ -55,14 +55,17 @@ Describe 'Get-FileHash2' { It 'Should stop with an error that names <_> in PowerShell 7' -Skip:(-not $isCore) -ForEach @('RIPEMD160', 'MACTripleDES') { $algorithm = $_ - { Get-FileHash2 -Path $first -Algorithm $algorithm -ErrorAction Stop } | - Should -Throw -ExpectedMessage "*'$algorithm'*Windows PowerShell 5.1*" + $hashError = { Get-FileHash2 -Path $first -Algorithm $algorithm -ErrorAction Stop } | + Should -Throw -ExpectedMessage "*'$algorithm'*Windows PowerShell 5.1*" -PassThru + + $hashError.FullyQualifiedErrorId | Should -BeLike 'HashAlgorithmNotAvailable,*' } - It 'Should warn that MACTripleDES is deprecated' -Skip:$isCore { - $result = Get-FileHash2 -Path $first -Algorithm MACTripleDES -WarningVariable hashWarnings -WarningAction SilentlyContinue + It 'Should warn once that MACTripleDES is deprecated' -Skip:$isCore { + $results = @(Get-FileHash2 -Path $first, $second -Algorithm MACTripleDES -WarningVariable hashWarnings -WarningAction SilentlyContinue) - $result.Hash | Should -Not -BeNullOrEmpty + $results | Should -HaveCount 2 + $results[0].Hash | Should -Not -BeNullOrEmpty $hashWarnings | Should -HaveCount 1 $hashWarnings[0].Message | Should -BeLike '*MACTripleDES*random key*deprecated*' } diff --git a/Tests/Inheritance.Tests.ps1 b/Tests/Inheritance.Tests.ps1 index b0fbc7b..19afd2c 100644 --- a/Tests/Inheritance.Tests.ps1 +++ b/Tests/Inheritance.Tests.ps1 @@ -126,6 +126,7 @@ Describe 'Set-NTFSInheritance' { # In memory, the kept entries stay marked as inherited; Windows stores them as explicit ones on write. It 'Should keep the inherited access entries of a security descriptor' { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepDescriptor' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file $sd = Get-NTFSSecurityDescriptor -Path $file $sidType = [System.Security.Principal.SecurityIdentifier] $inheritedCount = @($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)).Count @@ -214,4 +215,4 @@ Describe 'Audit inheritance switches' { { & $Command @parameters -ErrorAction SilentlyContinue } | Should -Not -Throw } -} \ No newline at end of file +} diff --git a/Tests/OutputTypes.Tests.ps1 b/Tests/OutputTypes.Tests.ps1 index c767c41..7708b67 100644 --- a/Tests/OutputTypes.Tests.ps1 +++ b/Tests/OutputTypes.Tests.ps1 @@ -99,4 +99,4 @@ Describe 'Cmdlet classes' { (Get-Command -Name 'Remove-Item2').ImplementingType.GetField('filter', $flags) | Should -BeNullOrEmpty } -} \ No newline at end of file +}