From 987334e415cfd50c55e264a8c9a5334169bbecda Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 11:40:43 +0000 Subject: [PATCH 01/36] fix(security2): keep the descriptor in the conversions from FileSecurity and DirectorySecurity Converting a FileSecurity or DirectorySecurity object to FileSystemSecurity2 (for example `[Security2.FileSystemSecurity2] $fileSecurity`) always threw, because the conversion ignored the descriptor it was given and built the result from an empty path, which AlphaFS rejects. The conversion now wraps the descriptor that was passed: SecurityDescriptor is that same instance, IsFile reflects whether it came from a FileSecurity or a DirectorySecurity, the round trip back to FileSecurity/DirectorySecurity returns the same instance, and Equals/GetHashCode keep comparing descriptors by reference. The resulting object has no item, since it was never read from a file or folder: FullName and Name are now null instead of throwing a NullReferenceException, and Write() throws a clear InvalidOperationException instead of a NullReferenceException deep in SetSecurity. A caller can still write such an object by setting its Item property first, or by calling Write(FileSystemInfo) or Write(string) directly, neither of which depends on an item being set. No cmdlet reaches this conversion; only a .NET caller of Security2.dll that converts a FileSecurity/DirectorySecurity it built itself is affected. Public signatures are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Security2/FileSystem/FileSystemSecurity2.cs | 46 ++++++++-- Tests/ObjectApis.Tests.ps1 | 93 +++++++++++++++++++++ 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/Security2/FileSystem/FileSystemSecurity2.cs b/Security2/FileSystem/FileSystemSecurity2.cs index cf905ba..9e29088 100644 --- a/Security2/FileSystem/FileSystemSecurity2.cs +++ b/Security2/FileSystem/FileSystemSecurity2.cs @@ -22,9 +22,12 @@ namespace Security2 set { item = value; } } - public string FullName { get { return item.FullName; } } + // An object that wraps a descriptor directly, such as the conversion from FileSecurity or DirectorySecurity, + // has no item and so no path to report; null reads more naturally here than an empty string, which would look + // like the path of an item whose name happens to be empty. + public string FullName { get { return item != null ? item.FullName : null; } } - public string Name { get { return item.Name; } } + public string Name { get { return item != null ? item.Name : null; } } public bool IsFile { get { return isFile; } } @@ -39,6 +42,18 @@ namespace Security2 RememberSections(); } + // Wraps a descriptor that a caller created directly, such as the conversions from FileSecurity and + // DirectorySecurity do; the object has no item. The descriptor is treated as if every section had been read, + // since there was no partial read: WriteChanges compares against it as a whole. + private FileSystemSecurity2(FileSystemSecurity sd) + { + this.sd = sd; + isFile = sd is FileSecurity; + sections = AccessControlSections.All; + + RememberSections(); + } + public FileSystemSecurity2(FileSystemInfo item) { this.item = item; @@ -195,6 +210,8 @@ namespace Security2 // that is read alone, and writing them back fails for an owner that the user cannot assign (#34). public void Write() { + RequireItem(); + SetSecurity(item, sd, sections); RememberSections(); @@ -204,6 +221,8 @@ namespace Security2 // unchanged owner that the user cannot assign isn't written back (#34). Without a change, it writes nothing. internal void WriteChanges() { + RequireItem(); + var changedSections = ChangedSections; if (changedSections == AccessControlSections.None) { @@ -215,6 +234,20 @@ namespace Security2 RememberSections(); } + // Write() and WriteChanges() write to the item that the descriptor was read from; an object that a conversion + // from FileSecurity or DirectorySecurity created has none. Without this check they would fail with a + // NullReferenceException deep in SetSecurity instead of saying what is missing. + private void RequireItem() + { + if (item == null) + { + throw new InvalidOperationException( + "This security descriptor has no item to write to: it was created directly from a FileSecurity or " + + "DirectorySecurity object, not read from a file or folder. Set the Item property first, or call " + + "Write(FileSystemInfo) or Write(string) to choose where to write it."); + } + } + // Writes the sections that the descriptor was read with to another item, like Write(). public void Write(FileSystemInfo item) { @@ -243,14 +276,17 @@ namespace Security2 #region Conversion // The descriptor is in sd. Before 5.0.0-rc6, these conversions returned fields that were never set, so they - // gave null, and the hash code threw a NullReferenceException. + // gave null, and the hash code threw a NullReferenceException. Before 5.0.0, the conversions the other way + // (from FileSecurity/DirectorySecurity) ignored the descriptor that was passed and returned an object built + // from an empty path, which always threw; they now keep the descriptor that was passed, see the private + // constructor above. public static implicit operator FileSecurity(FileSystemSecurity2 fs2) { return fs2.sd as FileSecurity; } public static implicit operator FileSystemSecurity2(FileSecurity fs) { - return new FileSystemSecurity2(new FileInfo("")); + return new FileSystemSecurity2((FileSystemSecurity)fs); } public static implicit operator DirectorySecurity(FileSystemSecurity2 fs2) @@ -259,7 +295,7 @@ namespace Security2 } public static implicit operator FileSystemSecurity2(DirectorySecurity fs) { - return new FileSystemSecurity2(new DirectoryInfo("")); + return new FileSystemSecurity2((FileSystemSecurity)fs); } // Like the descriptors of .NET, two objects are equal when they wrap the same descriptor; a descriptor of .NET diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index 575e34c..eafb0e1 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -797,6 +797,99 @@ Describe 'Owner and descriptor objects' { } } +# Before 5.0.0, both conversions ignored the descriptor that was passed and returned a FileSystemSecurity2 that wrapped +# a FileInfo or a DirectoryInfo built from an empty path, which AlphaFS rejects, so the conversion always threw. The +# object that the fixed conversion returns has no item: FullName and Name, which have no path to report, are null, +# which reads most naturally as "nothing to name". +Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySecurity' { + BeforeAll { + function New-RawFileSystemSecurity { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([string] $Kind) + + $raw = New-Object -TypeName "System.Security.AccessControl.$Kind" + $rule = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $raw.AddAccessRule($rule) + $raw + } + } + + It 'Should wrap the descriptor that was passed, report IsFile as , and round-trip the same instance' -ForEach @( + @{ Kind = 'FileSecurity'; IsFile = $true } + @{ Kind = 'DirectorySecurity'; IsFile = $false } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $targetType = [type] "System.Security.AccessControl.$Kind" + + $wrapped = [Security2.FileSystemSecurity2] $raw + + [object]::ReferenceEquals($wrapped.SecurityDescriptor, $raw) | Should -BeTrue + $wrapped.IsFile | Should -Be $IsFile + [object]::ReferenceEquals(($wrapped -as $targetType), $raw) | Should -BeTrue + $wrapped.FullName | Should -Be $null + $wrapped.Name | Should -Be $null + } + + It 'Should compare two conversions of the same descriptor as equal, and of different descriptors as unequal' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + + $first = [Security2.FileSystemSecurity2] $raw + $second = [Security2.FileSystemSecurity2] $raw + $other = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $first.Equals($second) | Should -BeTrue + $first.GetHashCode() | Should -Be $second.GetHashCode() + $first.Equals($other) | Should -BeFalse + } + + It 'Should throw an InvalidOperationException, not a NullReferenceException, when a without an item is written' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $failure = { $wrapped.Write() } | Should -Throw -PassThru + + $failure.Exception.GetBaseException() | Should -BeOfType [System.InvalidOperationException] + $failure.Exception.GetBaseException().Message | Should -BeLike '*item*' + } + + It 'Should write a without an item once the caller sets Item' -ForEach @( + @{ Kind = 'FileSecurity'; Directory = $false } + @{ Kind = 'DirectorySecurity'; Directory = $true } + ) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWriteItem' -Directory:$Directory + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + $wrapped.Item = Get-Item2 -Path $path + + $wrapped.Write() + + @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | + Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 + } + + It 'Should write a without an item directly to a path' -ForEach @( + @{ Kind = 'FileSecurity'; Directory = $false } + @{ Kind = 'DirectorySecurity'; Directory = $true } + ) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWritePath' -Directory:$Directory + $wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $wrapped.Write([string] $path) + + @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | + Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 + } +} + Describe 'Generic access rights and identity errors' { It 'Should map the generic mask to the file system rights ' -ForEach @( @{ Mask = '80000000'; Expected = '00120089' } From a0a97d971e91f47e2f540c0cc912083f03c4de11 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 11:48:16 +0000 Subject: [PATCH 02/36] fix(security2): honor the account list of RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll Both methods built a LINQ query that filtered the explicit entries down to the ones whose account was in the given list, but never used the result: the loop right below removed every explicit entry of the descriptor regardless of the list, including an empty one. The cmdlets never pass a list, so only a .NET caller of Security2.dll that does is affected. A non-null list now removes only the explicit entries whose account is one of the listed accounts (compared like the rest of the class does, by IdentityReference2 equality of the entry's SID); null still removes every explicit entry, and an empty list now removes none. Inherited entries are still never touched, as before. Both the FileSystemSecurity2 overload (in-memory only, until a caller writes the descriptor) and the FileSystemInfo overload (which also writes) are fixed, for access and audit entries alike. Public signatures are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- ...essRule2.RemoveFileSystemAccessRulesAll.cs | 14 +- ...AuditRule2.RemoveFileSystemAuditRuleAll.cs | 14 +- Tests/ObjectApis.Tests.ps1 | 165 ++++++++++++++++++ 3 files changed, 183 insertions(+), 10 deletions(-) diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs index bcc1c27..45cd7e0 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRulesAll.cs @@ -8,16 +8,20 @@ namespace Security2 { public partial class FileSystemAccessRule2 { + /// + /// Removes the explicit access entries of the descriptor; with a non-null , only + /// the entries whose account is in the list (compared like IdentityReference2.Equals), leaving the rest of + /// the descriptor untouched. Before 5.0.0, the list was never applied: the filtered result was discarded, and + /// every explicit entry was removed regardless of the list, including with an empty list. + /// public static void RemoveFileSystemAccessRuleAll(FileSystemSecurity2 sd, List accounts = null) { var acl = sd.SecurityDescriptor.GetAccessRules(true, false, typeof(SecurityIdentifier)); - if (accounts != null) - { - acl.OfType().Where(ace => (accounts.Where(account => account == (IdentityReference2)ace.IdentityReference).Count() > 1)); - } + var entriesToRemove = accounts == null ? acl.OfType() : + acl.OfType().Where(ace => accounts.Any(account => account == (IdentityReference2)ace.IdentityReference)); - foreach (FileSystemAccessRule ace in acl) + foreach (FileSystemAccessRule ace in entriesToRemove.ToList()) { sd.SecurityDescriptor.RemoveAccessRuleSpecific(ace); } diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs index ee721f0..f1872df 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.RemoveFileSystemAuditRuleAll.cs @@ -8,16 +8,20 @@ namespace Security2 { public partial class FileSystemAuditRule2 { + /// + /// Removes the explicit audit entries of the descriptor; with a non-null , only + /// the entries whose account is in the list (compared like IdentityReference2.Equals), leaving the rest of + /// the descriptor untouched. Before 5.0.0, the list was never applied: the filtered result was discarded, and + /// every explicit entry was removed regardless of the list, including with an empty list. + /// public static void RemoveFileSystemAuditRuleAll(FileSystemSecurity2 sd, List accounts = null) { var acl = sd.SecurityDescriptor.GetAuditRules(true, false, typeof(SecurityIdentifier)); - if (accounts != null) - { - acl.OfType().Where(ace => (accounts.Where(account => account == (IdentityReference2)ace.IdentityReference).Count() > 1)); - } + var entriesToRemove = accounts == null ? acl.OfType() : + acl.OfType().Where(ace => accounts.Any(account => account == (IdentityReference2)ace.IdentityReference)); - foreach (FileSystemAuditRule ace in acl) + foreach (FileSystemAuditRule ace in entriesToRemove.ToList()) { sd.SecurityDescriptor.RemoveAuditRuleSpecific(ace); } diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index eafb0e1..e2fc353 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -890,6 +890,171 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec } } +# Before 5.0.0, the account list of both methods was built but its filter result was never assigned anywhere, so a +# non-null list removed every explicit entry regardless of what it named; only a .NET caller that passes a list is +# affected, since the cmdlets never do. +Describe 'RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll honor the account list' { + BeforeAll { + $allow = [System.Security.AccessControl.AccessControlType]::Allow + $success = [System.Security.AccessControl.AuditFlags]::Success + $accountOne = $identity + $accountTwo = [Security2.IdentityReference2] 'S-1-5-32-545' + $accountThree = [Security2.IdentityReference2] 'S-1-5-32-544' + $accountsByLabel = @{ One = $accountOne; Two = $accountTwo; Three = $accountThree } + + function New-AccessSecurityFor { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity' + foreach ($account in $Accounts) { + $raw.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $allow + ))) + } + $raw + } + + function New-AuditSecurityFor { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity' + foreach ($account in $Accounts) { + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $success + ))) + } + $raw + } + + function New-IdentityList { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates a list.' + )] + param ([Security2.IdentityReference2[]] $Accounts) + + $list = New-Object -TypeName 'System.Collections.Generic.List[Security2.IdentityReference2]' + foreach ($account in $Accounts) { + $list.Add($account) + } + , $list + } + + function Get-AccessSddl { + param ([System.Security.AccessControl.FileSecurity] $Raw) + + $Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) + } + + function Get-AuditSddl { + param ([System.Security.AccessControl.FileSecurity] $Raw) + + $Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Audit) + } + + # Builds the expected result by removing the given accounts with the .NET method directly, instead of with + # a freshly built descriptor that only holds the remaining accounts: once an entry was added and removed + # again, the SACL stays present but empty, while a SACL that never held an entry is absent, and the two give + # a different SDDL for the same set of remaining entries. + function Get-ExpectedAccessSddl { + param ([Security2.IdentityReference2[]] $AccountsToRemove) + + $raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + foreach ($account in $AccountsToRemove) { + $raw.RemoveAccessRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $allow + ))) + } + Get-AccessSddl $raw + } + + function Get-ExpectedAuditSddl { + param ([Security2.IdentityReference2[]] $AccountsToRemove) + + $raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + foreach ($account in $AccountsToRemove) { + $raw.RemoveAuditRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + [System.Security.Principal.SecurityIdentifier] $account, + [System.Security.AccessControl.FileSystemRights]::ReadData, $success + ))) + } + Get-AuditSddl $raw + } + } + + It 'Should honor the account list when it removes access entries from a descriptor with three accounts, in memory, for ' -ForEach @( + @{ Scenario = 'a list with one account'; AccountsToRemove = @('One') } + @{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') } + @{ Scenario = 'a null list'; AccountsToRemove = $null } + @{ Scenario = 'an empty list'; AccountsToRemove = @() } + ) { + $raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + $toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + $expected = Get-ExpectedAccessSddl -AccountsToRemove $toRemove + $accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + + [Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts) + + Get-AccessSddl $raw | Should -BeExactly $expected + } + + It 'Should honor the account list when it removes audit entries from a descriptor with three accounts, in memory, for ' -ForEach @( + @{ Scenario = 'a list with one account'; AccountsToRemove = @('One') } + @{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') } + @{ Scenario = 'a null list'; AccountsToRemove = $null } + @{ Scenario = 'an empty list'; AccountsToRemove = @() } + ) { + $raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree + $toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + $expected = Get-ExpectedAuditSddl -AccountsToRemove $toRemove + $accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) } + + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts) + + Get-AuditSddl $raw | Should -BeExactly $expected + } + + It 'Should remove only the listed account''s explicit access entries of a sandbox item, read back with .NET' { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAccessByItem' + $item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path + foreach ($account in $accountOne, $accountTwo, $accountThree) { + [void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule( + $item, $account, [Security2.FileSystemRights2]::ReadData, $allow, + [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None + ) + } + + [Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll($item, (New-IdentityList -Accounts $accountOne)) + + $remaining = @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + @($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object) + } + + It 'Should remove only the listed account''s explicit audit entries of a sandbox item, read back with .NET' -Skip:(-not $holdsSecurityPrivilege) { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAuditByItem' + $item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path + foreach ($account in $accountOne, $accountTwo, $accountThree) { + [void] [Security2.FileSystemAuditRule2]::AddFileSystemAuditRule( + $item, $account, [Security2.FileSystemRights2]::ReadData, $success, + [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None + ) + } + + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll($item, (New-IdentityList -Accounts $accountOne)) + + $remaining = @((Get-Acl -LiteralPath $path -Audit).GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + @($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object) + } +} + Describe 'Generic access rights and identity errors' { It 'Should map the generic mask to the file system rights ' -ForEach @( @{ Mask = '80000000'; Expected = '00120089' } From a0362c359c57ce1fc8a576fa292ef4a9d28c7110 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 11:51:53 +0000 Subject: [PATCH 03/36] fix(security2): make the path overloads of AddFileSystemAccessRule and AddFileSystemAuditRule write at once AddFileSystemAccessRule(string, List, ...) and AddFileSystemAuditRule(string, List, ...) were iterator methods (yield return), so nothing happened until the caller enumerated the result: called as a statement, or assigned to a variable that the caller never reads, they changed nothing. Their siblings, the overloads for FileSystemInfo and FileSystemSecurity2 with a list, and the single-account overloads, already built a list and wrote at once. Both overloads now build the list and add the entries before returning, like their siblings; a call that is never enumerated now adds the entries just the same. An exception that adding an entry raises changes only in timing: it is now thrown by the call itself, not by the later enumeration of its result. The returned collection still holds one rule per account, in the order of the account list, for both a file path and a folder path. Public signatures are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- ...temAccessRule2.AddFileSystemAccessRules.cs | 16 ++++++++++-- ...ystemAuditRule2.AddFileSystemAuditRules.cs | 16 ++++++++++-- Tests/ObjectApis.Tests.ps1 | 25 ++++++++++++++----- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs index cd8f067..d5ba957 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.AddFileSystemAccessRules.cs @@ -87,17 +87,27 @@ namespace Security2 return ace; } + /// + /// Adds one access entry per account to the item at the path, and returns them in the order of + /// . Before 5.0.0, this overload was an iterator (a yield return in the loop + /// below), so nothing happened until the caller enumerated the result; called as a statement, or assigned to + /// a variable that is never read, it changed nothing. It now builds the list and writes the entries before + /// returning, like its FileSystemInfo and FileSystemSecurity2 siblings; an exception that adding an entry + /// raises is now thrown by this call, not by the later enumeration. + /// public static IEnumerable AddFileSystemAccessRule(string path, List accounts, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags) { if (type == AccessControlType.Allow) rights = rights | FileSystemRights2.Synchronize; + var aces = new List(); + if (File.Exists(path)) { var item = new FileInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } else @@ -105,9 +115,11 @@ namespace Security2 var item = new DirectoryInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAccessRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } + + return aces; } public static void AddFileSystemAccessRule(FileSystemAccessRule2 rule) diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs index 59e1b9b..5ed1c8a 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.AddFileSystemAuditRules.cs @@ -78,14 +78,24 @@ namespace Security2 return aces; } + /// + /// Adds one audit entry per account to the item at the path, and returns them in the order of + /// . Before 5.0.0, this overload was an iterator (a yield return in the loop + /// below), so nothing happened until the caller enumerated the result; called as a statement, or assigned to + /// a variable that is never read, it changed nothing. It now builds the list and writes the entries before + /// returning, like its FileSystemInfo and FileSystemSecurity2 siblings; an exception that adding an entry + /// raises is now thrown by this call, not by the later enumeration. + /// public static IEnumerable AddFileSystemAuditRule(string path, List accounts, FileSystemRights2 rights, AuditFlags type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags) { + var aces = new List(); + if (File.Exists(path)) { var item = new FileInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } else @@ -93,9 +103,11 @@ namespace Security2 var item = new DirectoryInfo(path); foreach (var account in accounts) { - yield return AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags); + aces.Add(AddFileSystemAuditRule(item, account, rights, type, inheritanceFlags, propagationFlags)); } } + + return aces; } public static void AddFileSystemAuditRule(FileSystemAuditRule2 rule) diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index e2fc353..76f65ce 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -395,9 +395,11 @@ Describe 'Access rule helpers that take a path' { $entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights] 'ReadData, Synchronize') } - # The overload that takes a path returns an iterator, so the caller must enumerate the result to write the entries. - # The overloads that take an item write them at once; this test doesn't pin the difference. - It 'Should add the entries of several accounts to a by its path when the result is enumerated' -ForEach @( + # Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the + # caller enumerated the result, so a call as a statement, or an assignment that the caller never reads, wrote + # nothing; the overloads that take an item or a FileSystemSecurity2 were never lazy. It now writes at once, like + # its siblings, so the entries exist before $pending is ever touched. + It 'Should add the entries of several accounts to a by its path at once, before the result is ever enumerated' -ForEach @( @{ Kind = 'file'; Directory = $false } @{ Kind = 'folder'; Directory = $true } ) { @@ -408,9 +410,13 @@ Describe 'Access rule helpers that take a path' { $path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation ) - @($pending) | Should -HaveCount 2 @(Get-ExplicitEntries -Path $path) | Should -HaveCount 1 @(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + + $added = @($pending) + $added | Should -HaveCount 2 + $added[0].Account.Sid | Should -BeExactly $identity.Sid + $added[1].Account.Sid | Should -BeExactly $users.Sid } It 'Should add the deny entries of several accounts without Synchronize when the result is enumerated' { @@ -589,7 +595,10 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile $found[0].FullName | Should -BeExactly $path } - It 'Should add the entries of several accounts to a by its path when the result is enumerated, and remove them again' -ForEach @( + # Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the + # caller enumerated the result; it now writes at once, like its siblings, so the entries exist before $pending is + # ever touched. This overload needs the Security privilege like its neighbors, so the whole Describe skips without it. + It 'Should add the entries of several accounts to a by its path at once, before the result is ever enumerated, and remove them again' -ForEach @( @{ Kind = 'file'; Directory = $false } @{ Kind = 'folder'; Directory = $true } ) { @@ -602,9 +611,13 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile $path, $accounts, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation ) - @($pending) | Should -HaveCount 2 @(Get-AuditEntries -Path $path) | Should -HaveCount 1 @(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + $added = @($pending) + $added | Should -HaveCount 2 + $added[0].Account.Sid | Should -BeExactly $identity.Sid + $added[1].Account.Sid | Should -BeExactly $users.Sid + [Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule( $path, $identity, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation, $true ) From b69b21e14bbebccfeac9c2d8cd5e60695b1c20a4 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 11:56:36 +0000 Subject: [PATCH 04/36] fix(privileges): report a malformed EnablePrivileges setting with one clear error BaseCmdletWithPrivControl.BeginProcessing and EndProcessing read (bool)privateData["EnablePrivileges"] without checking it first; a setting that is not a Boolean, such as the string '$true' from a typo in the module manifest, or a missing key, made every cmdlet derived from the class fail with an InvalidCastException or a NullReferenceException before anything useful was said. Enable-Privileges had its own try/catch for the same cast that raised a different message (a ParseException), but it was unreachable, because BeginProcessing always failed first. A new helper, GetEnablePrivilegesSetting, centralizes the check in the base class: a setting that is missing or not a Boolean now stops the cmdlet with one terminating error, id EnablePrivilegesSettingInvalid, category InvalidData, that names the setting, says it must be $true or $false, and names the module manifest. BeginProcessing, EndProcessing, and Enable-Privileges all use it now; the dead try/catch of Enable-Privileges is removed, so a mistaken setting no longer produces two different messages depending on which cmdlet hit it first. The valid settings $true and $false keep today's behavior. GetInheritedFrom (used by Get-NTFSAccess and Get-NTFSAudit), GetFileSystemModeProperty, and IdentifyHardLinks (both used by Get-ChildItem2) read their own setting the same unchecked way and share this weakness; they are not changed here; see the final report. Breaking: a script that caught the old InvalidCastException, NullReferenceException, or ParseException for a malformed EnablePrivileges setting now needs to catch the terminating error with id EnablePrivilegesSettingInvalid instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Concepts.md | 6 ++++++ NTFSSecurity/BaseCmdlets.cs | 35 +++++++++++++++++++++++++++++----- NTFSSecurity/OtherCmdlets.cs | 13 ++++--------- Tests/Privileges.Tests.ps1 | 37 ++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/Docs/Concepts.md b/Docs/Concepts.md index e63abbc..aeca9c3 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -274,6 +274,12 @@ change the module's behavior: cost extra work for every item. Turn them off to speed up large folder trees when you don't need the information. +A malformed `EnablePrivileges` setting, such as a string instead of +`$true` or `$false`, or a missing key, stops every cmdlet that uses +privileges, and `Enable-Privileges`, with the error +`EnablePrivilegesSettingInvalid` naming the setting, instead of an +unrelated exception. + To change a setting for the current session only, change the value after you import the module: diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 28a2164..528d315 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -354,7 +354,6 @@ namespace NTFSSecurity protected PrivilegeAndAttributesCollection privileges = null; protected PrivilegeControl privControl = new PrivilegeControl(); private List enabledPrivileges = new List(); - Hashtable privateData = null; // A cmdlet that enables the privileges for the session, such as Enable-Privileges, keeps them enabled. protected virtual bool KeepEnabledPrivileges @@ -364,9 +363,7 @@ namespace NTFSSecurity protected override void BeginProcessing() { - privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; - - if ((bool)privateData["EnablePrivileges"]) + if (GetEnablePrivilegesSetting()) { WriteVerbose("EnablePrivileges enabled in PrivateDate"); EnableFileSystemPrivileges(true); @@ -375,7 +372,7 @@ namespace NTFSSecurity protected override void EndProcessing() { - if ((bool)privateData["EnablePrivileges"]) + if (GetEnablePrivilegesSetting()) { WriteVerbose("EnablePrivileges enabled in PrivateDate"); @@ -395,6 +392,34 @@ namespace NTFSSecurity } } + /// + /// Reads the EnablePrivileges setting of the module manifest (NTFSSecurity.psd1, section PrivateData) and + /// terminates with one EnablePrivilegesSettingInvalid error when the setting is missing or is not a Boolean. + /// Before 5.0.0, BeginProcessing, EndProcessing, and Enable-Privileges each cast the setting on their own + /// without checking it first, so a missing key or a value that was not a Boolean, such as the string '$true' + /// from a typo in the manifest, failed with a NullReferenceException or an InvalidCastException before + /// anything useful was said, and Enable-Privileges raised a second, different message for the same cause + /// that was never reached, because BeginProcessing always failed first. + /// + protected bool GetEnablePrivilegesSetting() + { + var moduleData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; + var value = moduleData == null ? null : moduleData["EnablePrivileges"]; + + if (!(value is bool)) + { + var found = value == null ? "missing" : string.Format("'{0}'", value); + var message = string.Format( + "The 'EnablePrivileges' setting in the PrivateData section of the module manifest (NTFSSecurity.psd1) " + + "must be $true or $false, but it is {0}. See the NTFSSecurity documentation for EnablePrivileges.", + found); + + ThrowTerminatingError(new ErrorRecord(new ArgumentException(message), "EnablePrivilegesSettingInvalid", ErrorCategory.InvalidData, value)); + } + + return (bool)value; + } + // PowerShell calls Dispose also when a later command or a terminating error stops the pipeline, and then // skips EndProcessing. Before 5.0.0-rc6, the privileges that the cmdlet had enabled stayed enabled in the // session in that case. PowerShell ignores an exception from Dispose and no stream is open anymore, so a diff --git a/NTFSSecurity/OtherCmdlets.cs b/NTFSSecurity/OtherCmdlets.cs index fd21efd..79f5c72 100644 --- a/NTFSSecurity/OtherCmdlets.cs +++ b/NTFSSecurity/OtherCmdlets.cs @@ -29,17 +29,12 @@ namespace NTFSSecurity protected override void ProcessRecord() { - var privateData = (System.Collections.Hashtable)this.MyInvocation.MyCommand.Module.PrivateData; var psCallStack = (CallStackFrame)this.InvokeCommand.InvokeScript("Get-PSCallStack")[1].BaseObject; - try - { - enablePrivileges = (bool)privateData["EnablePrivileges"]; - } - catch (Exception ex) - { - throw new ParseException("Could not parse the module's PrivateData field in the module's psd1 file. Please refer to the documentation for further details", ex); - } + // The base class raises one EnablePrivilegesSettingInvalid error for a setting that is not $true or + // $false; before 5.0.0, this had its own try/catch that raised a ParseException for the same cast, but + // it never ran, because BeginProcessing always failed first with its own uncaught cast. + enablePrivileges = GetEnablePrivilegesSetting(); //if the command is called from NTFSSecurity.Init.ps1 and EnablePrivileges is set to true in the NTFSSecurity.psd1 or if the cmdlet is called from somewhere else if ((psCallStack.InvocationInfo.MyCommand.Name == "NTFSSecurity.Init.ps1" && enablePrivileges == true)) diff --git a/Tests/Privileges.Tests.ps1 b/Tests/Privileges.Tests.ps1 index 8d5e219..812e44f 100644 --- a/Tests/Privileges.Tests.ps1 +++ b/Tests/Privileges.Tests.ps1 @@ -663,3 +663,40 @@ Describe 'The PrivilegeControl class' { Get-BackupPrivilegeState | Should -Be 'Disabled' } } + +# Before 5.0.0, a module setting that was not a Boolean (a typo such as the string '$true', or a missing key) made +# every cmdlet derived from BaseCmdletWithPrivControl fail in BeginProcessing with an InvalidCastException or a +# NullReferenceException before anything useful was said, and Enable-Privileges had its own try/catch for the same +# cast that never ran, because BeginProcessing always failed first. One EnablePrivilegesSettingInvalid error now +# names the setting instead, for a cmdlet and for Enable-Privileges alike. The test needs no elevation. +Describe 'A malformed EnablePrivileges setting' { + BeforeAll { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'MalformedSetting' + } + + It 'Should stop a cmdlet and Enable-Privileges with one EnablePrivilegesSettingInvalid error for , not an InvalidCastException or a NullReferenceException' -ForEach @( + @{ Case = 'a string value'; Setting = 'maybe' } + @{ Case = 'a missing value'; Setting = $null } + ) { + $privateData['EnablePrivileges'] = $Setting + + try { + $cmdletFailure = { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Throw -PassThru + $enableFailure = { Enable-Privileges -ErrorAction Stop } | Should -Throw -PassThru + } + finally { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + foreach ($failure in $cmdletFailure, $enableFailure) { + $failure.FullyQualifiedErrorId | Should -BeLike 'EnablePrivilegesSettingInvalid,*' + $failure.Exception.Message | Should -BeLike '*EnablePrivileges*' + $failure.Exception | Should -Not -BeOfType [System.InvalidCastException] + $failure.Exception | Should -Not -BeOfType [System.NullReferenceException] + } + + # Restoring the setting must leave the session usable again, for this test and the ones that follow it. + $privateData['EnablePrivileges'] | Should -Be $enablePrivileges + { Get-NTFSInheritance -Path $file -ErrorAction Stop } | Should -Not -Throw + } +} From 3f146b8e52842825ba7b147940df40e641d36421 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:38:34 +0000 Subject: [PATCH 05/36] fix(security2): let the readers handle a descriptor that has no item 3.1 made a FileSystemSecurity2 without an item reachable from a .NET caller (the conversions from FileSecurity and DirectorySecurity), but six sibling call sites still dereferenced the item without checking it first, so a caller who converted a FileSecurity or DirectorySecurity and then read its entries, owner, inheritance, or effective access met a NullReferenceException instead of a usable result. GetFileSystemAccessRules and GetFileSystemAuditRules (the FileSystemSecurity2 overload) now give each returned entry the same null FullName that the descriptor itself reports; asking for the source of an inherited entry (getInheritedFrom) needs the path of the item, so it now throws one clear InvalidOperationException instead, naming the reason, when the descriptor has none. FileSystemOwner and FileSystemInheritanceInfo give a null FullName (and Name) the same way; FileSystemOwner.ToString, which must never return null, gives an empty string instead, since there is no other representation to fall back to. The FileSystemAccessRule2(rule, item) constructor, which EffectiveAccess.GetEffectiveAccess uses to build its result, now accepts a null item the same way, so effective access can be calculated for such a descriptor and the result names no path. Every existing behavior for an item that is not null is unchanged; only a .NET caller that reads such a descriptor can notice the difference, and only for the better: a usable result or a clear error instead of an unrelated NullReferenceException. Public signatures are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- ...temAccessRule2.GetFileSystemAccessRules.cs | 13 +- .../FileSystemAccessRule2.cs | 4 +- ...ystemAuditRule2.GetFileSystemAuditRules.cs | 13 +- .../FileSystem/FileSystemInheritanceInfo.cs | 6 +- Security2/FileSystem/FileSystemOwner.cs | 8 +- Tests/ObjectApis.Tests.ps1 | 115 ++++++++++++++++++ 6 files changed, 152 insertions(+), 7 deletions(-) diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs index 8946af0..6e4f65f 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs @@ -1,4 +1,5 @@ using Alphaleonis.Win32.Filesystem; +using System; using System.Collections.Generic; using System.Security.AccessControl; using System.Security.Principal; @@ -21,6 +22,15 @@ namespace Security2 if (getInheritedFrom) { + // The source of an inherited entry is found from the path of the item that the descriptor was read + // from; a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. + if (sd.Item == null) + { + throw new InvalidOperationException( + "Cannot find the source of an inherited entry: this security descriptor has no item, so there " + + "is no path to search from. Pass getInheritedFrom as false, or read the entries from an item."); + } + inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, false); } @@ -40,7 +50,8 @@ namespace Security2 continue; } - var ace2 = new FileSystemAccessRule2(ace) { FullName = sd.Item.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAccessRulesProtected }; + // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. + var ace2 = new FileSystemAccessRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAccessRulesProtected }; if (getInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs index 4b1a200..fdeb63e 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs @@ -31,7 +31,9 @@ namespace Security2 public FileSystemAccessRule2(FileSystemAccessRule fileSystemAccessRule, FileSystemInfo item) { this.fileSystemAccessRule = fileSystemAccessRule; - this.fullName = item.FullName; + // Null without an item, such as when EffectiveAccess.GetEffectiveAccess builds this from a + // FileSystemSecurity2 that has none, instead of a NullReferenceException. + this.fullName = item != null ? item.FullName : null; } public FileSystemAccessRule2(FileSystemAccessRule fileSystemAccessRule, string path) diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs index fa18d6c..2d3d27c 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs @@ -1,4 +1,5 @@ using Alphaleonis.Win32.Filesystem; +using System; using System.Collections.Generic; using System.Security.AccessControl; using System.Security.Principal; @@ -21,6 +22,15 @@ namespace Security2 if (getInheritedFrom) { + // The source of an inherited entry is found from the path of the item that the descriptor was read + // from; a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. + if (sd.Item == null) + { + throw new InvalidOperationException( + "Cannot find the source of an inherited entry: this security descriptor has no item, so there " + + "is no path to search from. Pass getInheritedFrom as false, or read the entries from an item."); + } + inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, true); } @@ -40,7 +50,8 @@ namespace Security2 continue; } - var ace2 = new FileSystemAuditRule2(ace) { FullName = sd.Item.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAuditRulesProtected }; + // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. + var ace2 = new FileSystemAuditRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAuditRulesProtected }; if (getInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. diff --git a/Security2/FileSystem/FileSystemInheritanceInfo.cs b/Security2/FileSystem/FileSystemInheritanceInfo.cs index 5fc9f9c..3a37496 100644 --- a/Security2/FileSystem/FileSystemInheritanceInfo.cs +++ b/Security2/FileSystem/FileSystemInheritanceInfo.cs @@ -35,9 +35,11 @@ namespace Security2 set { auditInheritanceEnabled = value; } } - public string FullName { get { return Item.FullName; } } + // Null without an item, such as for a descriptor that a conversion from FileSecurity built directly, instead + // of a NullReferenceException. + public string FullName { get { return item != null ? item.FullName : null; } } - public string Name { get { return Path.GetFileName(item.FullName); } } + public string Name { get { return item != null ? Path.GetFileName(item.FullName) : null; } } private FileSystemInheritanceInfo(FileSystemInfo item, bool? accessInheritanceEnabled, bool? auditInheritanceEnabled) { diff --git a/Security2/FileSystem/FileSystemOwner.cs b/Security2/FileSystem/FileSystemOwner.cs index 103482b..a2dfea2 100644 --- a/Security2/FileSystem/FileSystemOwner.cs +++ b/Security2/FileSystem/FileSystemOwner.cs @@ -27,7 +27,9 @@ namespace Security2 public string FullName { - get { return item.FullName; } + // Null without an item, such as for a descriptor that a conversion from FileSecurity built directly, + // instead of a NullReferenceException. + get { return item != null ? item.FullName : null; } } private FileSystemOwner(FileSystemInfo item, IdentityReference2 owner) @@ -62,7 +64,9 @@ namespace Security2 public override string ToString() { - return item.FullName; + // Empty, not null, without an item: ToString must never return null, and there is no other + // representation of this object to fall back to. + return item != null ? item.FullName : string.Empty; } } } diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index 76f65ce..9f13b07 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -830,6 +830,18 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec $raw.AddAccessRule($rule) $raw } + + # PowerShell silently turns an exception that a property getter throws into $null, without even recording a + # non-terminating error, so a plain "$object.Property" cannot tell a getter that throws from one that + # legitimately returns null. The getter is invoked through reflection instead, so that it throws normally. + function Invoke-PropertyGetter { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only reads a property through reflection.' + )] + param ([object] $InputObject, [string] $PropertyName) + + $InputObject.GetType().GetProperty($PropertyName).GetGetMethod().Invoke($InputObject, $null) + } } It 'Should wrap the descriptor that was passed, report IsFile as , and round-trip the same instance' -ForEach @( @@ -901,6 +913,109 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 } + + # 3.1 makes a FileSystemSecurity2 without an item reachable from a .NET caller; the readers below must not meet a + # NullReferenceException from it. Built in memory, so no privilege is needed to add the audit rule. + It 'Should return the entries of a without an item, with a null FullName' -ForEach @( + @{ Kind = 'FileSecurity'; RuleType = 'access' } + @{ Kind = 'DirectorySecurity'; RuleType = 'access' } + @{ Kind = 'FileSecurity'; RuleType = 'audit' } + @{ Kind = 'DirectorySecurity'; RuleType = 'audit' } + ) { + if ($RuleType -eq 'access') { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + $found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $false)) + } + else { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AuditFlags]::Success + ))) + $sd = [Security2.FileSystemSecurity2] $raw + $found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $false)) + } + + $found | Should -HaveCount 1 + $found[0].FullName | Should -Be $null + $found[0].Account.Sid | Should -BeExactly 'S-1-1-0' + } + + It 'Should throw an InvalidOperationException, not a NullReferenceException, for the source of an inherited entry of a descriptor without an item' -ForEach @( + @{ RuleType = 'access' } + @{ RuleType = 'audit' } + ) { + if ($RuleType -eq 'access') { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind 'FileSecurity') + $failure = { [Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true) } | + Should -Throw -PassThru + } + else { + $raw = New-RawFileSystemSecurity -Kind 'FileSecurity' + $raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::ReadData, + [System.Security.AccessControl.AuditFlags]::Success + ))) + $sd = [Security2.FileSystemSecurity2] $raw + $failure = { [Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true) } | + Should -Throw -PassThru + } + + $failure.Exception.GetBaseException() | Should -BeOfType [System.InvalidOperationException] + $failure.Exception.GetBaseException().Message | Should -BeLike '*item*' + } + + It 'Should return an owner object with a null FullName and an empty ToString for a without an item' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.SetOwner($sid) + $sd = [Security2.FileSystemSecurity2] $raw + + $owner = [Security2.FileSystemOwner]::GetOwner($sd) + + $owner.Owner.Sid | Should -BeExactly 'S-1-1-0' + { Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName') | Should -Be $null + $owner.ToString() | Should -BeExactly '' + } + + It 'Should return inheritance info with a null FullName and Name for a without an item' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind) + + $info = [Security2.FileSystemInheritanceInfo]::GetFileSystemInheritanceInfo($sd) + + { Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName' } | Should -Not -Throw + { Invoke-PropertyGetter -InputObject $info -PropertyName 'Name' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName') | Should -Be $null + (Invoke-PropertyGetter -InputObject $info -PropertyName 'Name') | Should -Be $null + $info.AccessInheritanceEnabled | Should -BeTrue + } + + # The Authz access check needs a descriptor with an owner and a group, like any real file has; the minimal + # descriptor of the other tests has neither, and the call fails with "The parameter is incorrect" for either. + It 'Should return the effective access of a without an item, with a null FullName on the result' -ForEach @( + @{ Kind = 'FileSecurity' } + @{ Kind = 'DirectorySecurity' } + ) { + $raw = New-RawFileSystemSecurity -Kind $Kind + $raw.SetOwner($sid) + $raw.SetGroup($sid) + $sd = [Security2.FileSystemSecurity2] $raw + + $result = [Security2.EffectiveAccess]::GetEffectiveAccess($sd, $identity, $null) + + if ($result.OperationFailed) { + Set-ItResult -Skipped -Because "the Authz call could not run in this configuration: $($result.AuthzException.Message)" + return + } + + $result.Ace.FullName | Should -Be $null + } } # Before 5.0.0, the account list of both methods was built but its filter result was never assigned anywhere, so a From e060c24f4d29d6859c5ee807368020fbede4da95 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 13:54:39 +0000 Subject: [PATCH 06/36] fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor PowerShell's parameter binder uses the conversion that 3.1 fixed for every -SecurityDescriptor parameter, so a descriptor that Get-Acl returns, never meant for these cmdlets, now binds where it used to be a binding error. Two more consequences of that surfaced: With the default module setting GetInheritedFrom ($true), Get-NTFSAccess, Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped with the InvalidOperationException that a previous fix added for a descriptor without an item; a cmdlet user has no parameter to avoid it. GetFileSystemAccessRules and GetFileSystemAuditRules now treat getInheritedFrom as false for such a descriptor instead of throwing: the entries come back with no source, the same as an entry whose source Windows cannot name. The private constructor that wraps a descriptor directly marked every section, including Audit, as read; Get-Acl never reads Audit unless -Audit is given. This made HasAuditSection wrongly report true, so the existing #109 check let Get-NTFSAudit go on with no audit data instead of reporting that the descriptor has none, and let Get-NTFSInheritance report AuditInheritanceEnabled as a real value instead of null (unknown). The constructor now includes Audit in the sections only when the descriptor's binary form actually carries a SACL, the same test HasSystemAcl already uses. Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess, Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a descriptor already worked; Set-NTFSSecurityDescriptor already reported a clear, non-terminating error. A review finding on the earlier fix is also addressed: two assertions read a property through reflection instead of a plain PowerShell property access, which silently turns a throwing getter into $null without a trace and so could not tell a getter that throws from one that legitimately returns null. Every existing behavior for an item that is not null is unchanged. Public signatures are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- ...temAccessRule2.GetFileSystemAccessRules.cs | 23 +++-- ...ystemAuditRule2.GetFileSystemAuditRules.cs | 23 +++-- Security2/FileSystem/FileSystemSecurity2.cs | 13 ++- Tests/ObjectApis.Tests.ps1 | 85 ++++++++++++++++--- 4 files changed, 103 insertions(+), 41 deletions(-) diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs index 6e4f65f..0408646 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.GetFileSystemAccessRules.cs @@ -1,5 +1,4 @@ using Alphaleonis.Win32.Filesystem; -using System; using System.Collections.Generic; using System.Security.AccessControl; using System.Security.Principal; @@ -20,17 +19,15 @@ namespace Security2 List aceList = new List(); List inheritedFrom = null; - if (getInheritedFrom) - { - // The source of an inherited entry is found from the path of the item that the descriptor was read - // from; a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. - if (sd.Item == null) - { - throw new InvalidOperationException( - "Cannot find the source of an inherited entry: this security descriptor has no item, so there " + - "is no path to search from. Pass getInheritedFrom as false, or read the entries from an item."); - } + // The source of an inherited entry is found from the path of the item that the descriptor was read from; + // a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. Such an + // entry gets no source, the same as one whose source Windows could not name: a cmdlet caller has no + // parameter to turn getInheritedFrom off (the module setting GetInheritedFrom is true by default), so an + // exception here would stop a cmdlet that a descriptor without an item was never meant to reach. + var findInheritedFrom = getInheritedFrom && sd.Item != null; + if (findInheritedFrom) + { inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, false); } @@ -43,7 +40,7 @@ namespace Security2 foreach (FileSystemAccessRule ace in acl) { - var source = getInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; + var source = findInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; aceCounter++; if (ace.IsInherited ? !includeInherited : !includeExplicit) { @@ -52,7 +49,7 @@ namespace Security2 // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. var ace2 = new FileSystemAccessRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAccessRulesProtected }; - if (getInheritedFrom && inheritedFrom.Count > 0) + if (findInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. ace2.inheritedFrom = string.IsNullOrEmpty(source) ? "" : source.TrimEnd('\\'); diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs index 2d3d27c..5df90c9 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.GetFileSystemAuditRules.cs @@ -1,5 +1,4 @@ using Alphaleonis.Win32.Filesystem; -using System; using System.Collections.Generic; using System.Security.AccessControl; using System.Security.Principal; @@ -20,17 +19,15 @@ namespace Security2 List aceList = new List(); List inheritedFrom = null; - if (getInheritedFrom) - { - // The source of an inherited entry is found from the path of the item that the descriptor was read - // from; a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. - if (sd.Item == null) - { - throw new InvalidOperationException( - "Cannot find the source of an inherited entry: this security descriptor has no item, so there " + - "is no path to search from. Pass getInheritedFrom as false, or read the entries from an item."); - } + // The source of an inherited entry is found from the path of the item that the descriptor was read from; + // a descriptor that a caller built directly, such as the conversion from FileSecurity, has none. Such an + // entry gets no source, the same as one whose source Windows could not name: a cmdlet caller has no + // parameter to turn getInheritedFrom off (the module setting GetInheritedFrom is true by default), so an + // exception here would stop a cmdlet that a descriptor without an item was never meant to reach. + var findInheritedFrom = getInheritedFrom && sd.Item != null; + if (findInheritedFrom) + { inheritedFrom = Win32.GetInheritedFrom(sd.Item, sd.SecurityDescriptor, true); } @@ -43,7 +40,7 @@ namespace Security2 foreach (FileSystemAuditRule ace in acl) { - var source = getInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; + var source = findInheritedFrom && aceCounter < inheritedFrom.Count ? inheritedFrom[aceCounter] : null; aceCounter++; if (ace.IsInherited ? !includeInherited : !includeExplicit) { @@ -52,7 +49,7 @@ namespace Security2 // sd.FullName, not sd.Item.FullName: null without an item instead of a NullReferenceException. var ace2 = new FileSystemAuditRule2(ace) { FullName = sd.FullName, InheritanceEnabled = !sd.SecurityDescriptor.AreAuditRulesProtected }; - if (getInheritedFrom && inheritedFrom.Count > 0) + if (findInheritedFrom && inheritedFrom.Count > 0) { // Windows names a folder with a trailing backslash; the text for an unknown parent has none. ace2.inheritedFrom = string.IsNullOrEmpty(source) ? "" : source.TrimEnd('\\'); diff --git a/Security2/FileSystem/FileSystemSecurity2.cs b/Security2/FileSystem/FileSystemSecurity2.cs index 9e29088..6e5b71e 100644 --- a/Security2/FileSystem/FileSystemSecurity2.cs +++ b/Security2/FileSystem/FileSystemSecurity2.cs @@ -43,13 +43,20 @@ namespace Security2 } // Wraps a descriptor that a caller created directly, such as the conversions from FileSecurity and - // DirectorySecurity do; the object has no item. The descriptor is treated as if every section had been read, - // since there was no partial read: WriteChanges compares against it as a whole. + // DirectorySecurity do; the object has no item. Access, Owner, and Group are always part of such a + // descriptor; Audit is part of it only when the descriptor actually carries a SACL (the same test as + // HasSystemAcl), since there was no read to tell sections apart otherwise. Without this, a descriptor that + // Get-Acl returned without -Audit reported HasAuditSection as true, so TestAuditSection (#109) let the audit + // cmdlets go on with no audit data at all, instead of reporting that the descriptor has none. private FileSystemSecurity2(FileSystemSecurity sd) { this.sd = sd; isFile = sd is FileSecurity; - sections = AccessControlSections.All; + sections = AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group; + if (new RawSecurityDescriptor(sd.GetSecurityDescriptorBinaryForm(), 0).SystemAcl != null) + { + sections |= AccessControlSections.Audit; + } RememberSections(); } diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index 9f13b07..2312e85 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -856,8 +856,13 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec [object]::ReferenceEquals($wrapped.SecurityDescriptor, $raw) | Should -BeTrue $wrapped.IsFile | Should -Be $IsFile [object]::ReferenceEquals(($wrapped -as $targetType), $raw) | Should -BeTrue - $wrapped.FullName | Should -Be $null - $wrapped.Name | Should -Be $null + # A plain property read turns a throwing getter into $null (see Invoke-PropertyGetter above), so these two + # getters are invoked through reflection: a null FullName/Name is pinned as the getter's own return value, + # not as whatever a getter that happened to throw would also look like from here. + { Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName' } | Should -Not -Throw + { Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'Name' } | Should -Not -Throw + (Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName') | Should -Be $null + (Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'Name') | Should -Be $null } It 'Should compare two conversions of the same descriptor as equal, and of different descriptors as unequal' -ForEach @( @@ -941,14 +946,17 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec $found[0].Account.Sid | Should -BeExactly 'S-1-1-0' } - It 'Should throw an InvalidOperationException, not a NullReferenceException, for the source of an inherited entry of a descriptor without an item' -ForEach @( + # Before this fix, a descriptor without an item made getInheritedFrom throw an InvalidOperationException, which a + # cmdlet caller has no parameter to avoid (the module setting GetInheritedFrom is $true by default): getInheritedFrom + # is now treated as false for such a descriptor instead, the same as an entry whose source Windows cannot name. + It 'Should return the entries with no source, not throw, when getInheritedFrom is requested for a descriptor without an item' -ForEach @( @{ RuleType = 'access' } @{ RuleType = 'audit' } ) { if ($RuleType -eq 'access') { $sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind 'FileSecurity') - $failure = { [Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true) } | - Should -Throw -PassThru + { [Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true) } | Should -Not -Throw + $found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true)) } else { $raw = New-RawFileSystemSecurity -Kind 'FileSecurity' @@ -957,12 +965,12 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec [System.Security.AccessControl.AuditFlags]::Success ))) $sd = [Security2.FileSystemSecurity2] $raw - $failure = { [Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true) } | - Should -Throw -PassThru + { [Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true) } | Should -Not -Throw + $found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true)) } - $failure.Exception.GetBaseException() | Should -BeOfType [System.InvalidOperationException] - $failure.Exception.GetBaseException().Message | Should -BeLike '*item*' + $found | Should -HaveCount 1 + $found[0].InheritedFrom | Should -Be $null } It 'Should return an owner object with a null FullName and an empty ToString for a without an item' -ForEach @( @@ -1018,9 +1026,62 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec } } -# Before 5.0.0, the account list of both methods was built but its filter result was never assigned anywhere, so a -# non-null list removed every explicit entry regardless of what it named; only a .NET caller that passes a list is -# affected, since the cmdlets never do. +# Review finding: PowerShell's binder uses the fixed conversion of 3.1 for every -SecurityDescriptor parameter, so a +# descriptor that Get-Acl returns, never meant for these cmdlets, now binds where it used to be a binding error. These +# pin the four cmdlets the review named; the one-line probes of the others are in the final report, not here. +Describe 'Cmdlets with a security descriptor that Get-Acl returns' { + BeforeAll { + $plainFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAclPlain' + Add-NTFSAccess -Path $plainFile -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly + } + + It 'Should return the explicit access entries and no error for Get-NTFSAccess -SecurityDescriptor (Get-Acl ...)' { + $sd = Get-Acl -LiteralPath $plainFile + $expected = @($sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])) + + $result = @(Get-NTFSAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorVariable accessErrors -ErrorAction SilentlyContinue) + + $accessErrors | Should -BeNullOrEmpty + ($result.Account.Sid | Sort-Object) | Should -Be ($expected.IdentityReference.Value | Sort-Object) + } + + It 'Should write the #109 ReadSecurityError, not an empty result, for Get-NTFSAudit -SecurityDescriptor (Get-Acl ...) without -Audit' { + $sd = Get-Acl -LiteralPath $plainFile + + $result = @(Get-NTFSAudit -SecurityDescriptor $sd -ErrorVariable auditErrors -ErrorAction SilentlyContinue) + + $result | Should -BeNullOrEmpty + $auditErrors | Should -HaveCount 1 + $auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidData' + $auditErrors[0].Exception.Message | Should -BeLike "*doesn't contain the audit entries*" + } + + It 'Should give a null AuditInheritanceEnabled and the right AccessInheritanceEnabled for Get-NTFSInheritance -SecurityDescriptor (Get-Acl ...)' { + $sd = Get-Acl -LiteralPath $plainFile + + $result = Get-NTFSInheritance -SecurityDescriptor $sd -ErrorAction Stop + + $result.AuditInheritanceEnabled | Should -Be $null + $result.AccessInheritanceEnabled | Should -Be (-not $sd.AreAccessRulesProtected) + } + + It 'Should write a non-terminating error naming the missing item for Set-NTFSSecurityDescriptor -SecurityDescriptor (Get-Acl ...), and change nothing' { + $sd = Get-Acl -LiteralPath $plainFile + $sd.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ( + $sid, [System.Security.AccessControl.FileSystemRights]::WriteData, + [System.Security.AccessControl.AccessControlType]::Allow + ))) + $before = (Get-Acl -LiteralPath $plainFile).Sddl + + $null = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable sdErrors -ErrorAction SilentlyContinue + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'WriteSdError,*' + $sdErrors[0].Exception.Message | Should -BeLike '*no item*' + (Get-Acl -LiteralPath $plainFile).Sddl | Should -BeExactly $before + } +} Describe 'RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll honor the account list' { BeforeAll { $allow = [System.Security.AccessControl.AccessControlType]::Allow From 522fd7fa06244cea94e2537cac2b63b4f0366e34 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:29:56 +0000 Subject: [PATCH 07/36] fix(items): match -Filter of Get-ChildItem2 with the dot rules of Windows PowerShell 5.1 Get-ChildItem2 -Filter now applies a single matcher, in the cmdlet, that follows the Win32 "DOS" wildcard rules Windows PowerShell 5.1 applies to Get-ChildItem -Filter, on the real name of the item, in both editions (Decision 27, item 4, option B). Before this change the pattern was matched twice: once by the AlphaFS enumeration and once by the cmdlet with a WildcardPattern that read a dot as an ordinary character, so the two editions, and Get-ChildItem itself, disagreed on patterns with a dot. AlphaFS now always enumerates with "*", and the new matcher decides alone. What a user sees changing: - "Report.*" also returns "Report", which has no dot (previously only names with a dot matched a pattern ending in ".*"). - A pattern that ends in a dot, such as "Report." or "Rep*.", now returns the names without a dot, instead of nothing. - "*.*.*" now returns every item, the same as "*.*" and a plain "*", instead of only names with two literal dots. - Breaking: an empty -Filter ('') now returns every item, like no filter at all, instead of nothing. This matches the policy of Windows PowerShell 5.1 for every other dot pattern; a script that relied on an empty -Filter returning nothing should pass -Filter '*' explicitly to keep that behavior from before. - A single dot ('.') as -Filter also returns every item, matching Get-ChildItem. - Get-ChildItem2 deliberately no longer follows PowerShell 7's own Get-ChildItem -Filter for a pattern that ends in a dot (which there returns nothing) or for "*.?" (which there returns every item): it follows the Windows PowerShell 5.1 rules in both editions instead, so the two editions of Get-ChildItem2 agree with each other. - Unaffected: -Filter matching by the real name and not the 8.3 short name, brackets and backticks as ordinary characters, case-insensitive matching with the invariant culture, the null-Filter error, and every other parameter and its interaction with -Filter. The matcher was derived and checked against a differential oracle of real Windows PowerShell 5.1 Get-ChildItem -Filter runs: two independent sets of 8.3-compliant names (22 and 19 names) against 54,654 generated patterns each (alphabet a, b, c, ., *, ? up to length 6; the remaining 1,333 of the 55,987 generated patterns per set are refused by the .NET enumeration API itself for ending in ".." and are not part of either product's -Filter semantics), with zero differences in both sets. Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type filters, and depth"): - Changed the dot-rules table (pins the new values for "Report.*", "Report.", "Rep*.", "*.", and an empty -Filter) and the dedicated "*.*.*" test (now expects every item), both of which pinned the old, changed-on-purpose behavior. - Added three rows to the existing short-name-sensitive test, to pin that the new matcher still reads the real name, not the short name, for the trailing-dot and "X.*" rule families. - Added a 36-row truth table (22 fixed 8.3-compliant names, patterns from every family the WI calls out) with the literal result of Windows PowerShell 5.1 as static, hand-verified data; the test runs no Get-ChildItem of its own. - Added -Recurse with a selective -Filter across three folder levels, a dot -Filter with -File and -Directory, an empty -Filter, and a -Filter on a -Path that names a file. Docs\Cmdlets\Get-ChildItem2.md: restated the -Filter parameter description and the matching note in NOTES with the Windows PowerShell 5.1 policy and the deliberate PowerShell 7 difference, and removed the sentences that described the dot rules as a limitation or a mismatch with Get-ChildItem in Windows PowerShell 5.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Get-ChildItem2.md | 4 +- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 213 ++++++++++++++++++++-- Tests/ItemCmdlets.Tests.ps1 | 147 +++++++++++++-- 3 files changed, 334 insertions(+), 30 deletions(-) diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 52275af..dccbb2e 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -142,7 +142,7 @@ Accept wildcard characters: False ### -Filter -Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. +Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name: an asterisk matches a run of characters, a question mark matches one character that is not a dot (nothing where a dot or the end of the name follows), and a dot also matches nothing where only wildcards or the end of the pattern follow, so that `Report.*` also returns `Report`, which has no dot, and `*.*` returns every item. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. PowerShell 7 deliberately differs here from the built-in `Get-ChildItem`, whose own `-Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`; `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. ```yaml Type: String @@ -319,7 +319,7 @@ Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an ` Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. -The names are matched twice, by the enumeration of the AlphaFS library and by the cmdlet, which reads a dot as an ordinary character, and their rules for a dot can differ from those of `Get-ChildItem`: a pattern such as `Report.*` does not return the file `Report`, which has no dot, a pattern that ends in a dot returns nothing, and an empty value returns nothing. Only the pattern of an asterisk, a dot, and an asterisk is treated as a single asterisk. +The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions (Decision 27, item 4): `Report.*` also returns `Report`, which has no dot, a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, like `*`. PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these two pattern shapes. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 66ad109..313d6d9 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -26,8 +26,6 @@ namespace NTFSSecurity private bool identifyHardLinks = false; private int? depth; - WildcardPattern wildcard = null; - System.Reflection.MethodInfo modeMethodInfo = null; [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -146,15 +144,9 @@ namespace NTFSSecurity paths = new List() { GetCurrentLocation() }; } - // Only * and ? are wildcards, like in the pattern that the enumeration matches; a bracket or a backtick stands - // for itself. Before 5.0.0, [1] was read as a character class, so a file with brackets in its name was not - // returned for its name. The enumeration returns every item for *.* as Windows does, so the comparison does - // too; with the dot as an ordinary character, it would drop the items without a dot, most folders. - // CultureInvariant: like the enumeration (see MoveNextInvariant), the comparison would otherwise treat I and i as - // different letters with tr-TR and drop items that the enumeration returned. - var pattern = filter == "*.*" ? "*" : filter; - wildcard = new WildcardPattern(pattern.Replace("`", "``").Replace("[", "`[").Replace("]", "`]"), WildcardOptions.Compiled | WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); - + // The names are matched by DosWildcard, not by the enumeration (see WriteFileSystem, which always asks + // AlphaFS for "*"), so there is nothing to pre-compile here: DosWildcard reads the raw value of -Filter + // at the time each name is compared. modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode"); getFileSystemModeProperty = (bool)((Hashtable)MyInvocation.MyCommand.Module.PrivateData)["GetFileSystemModeProperty"]; @@ -211,7 +203,10 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateDirectories(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + // The enumeration applies no pattern of its own; DosWildcard decides in + // WriteFileSystemInfoCollection, on the real name, with the dot rules of Windows + // PowerShell 5.1. + files = di.EnumerateDirectories("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -221,7 +216,7 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateFiles(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + files = di.EnumerateFiles("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -231,7 +226,7 @@ namespace NTFSSecurity IEnumerator files; using (new InvariantCultureScope()) { - files = di.EnumerateFileSystemInfos(filter, global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); + files = di.EnumerateFileSystemInfos("*", global::System.IO.SearchOption.TopDirectoryOnly).GetEnumerator(); } WriteFileSystemInfoCollection(files); @@ -304,7 +299,7 @@ namespace NTFSSecurity while (MoveNextInvariant(fileSystemInfos)) { FileSystemInfo current = (FileSystemInfo)fileSystemInfos.Current; - if (!wildcard.IsMatch(current.Name)) + if (!DosWildcard.IsMatch(current.Name, filter)) { continue; } @@ -401,5 +396,193 @@ namespace NTFSSecurity global::System.Threading.Thread.CurrentThread.CurrentCulture = previous; } } + + // Matches a -Filter pattern against the real name of an item with the Win32 "DOS" wildcard rules that + // Windows PowerShell 5.1 applies to Get-ChildItem -Filter, in both editions, so that the two editions and + // the short-name based matching that a past fix removed do not disagree. The rules come from + // FsRtlIsNameInExpression with the Win32 translation of a DOS pattern (DOS_STAR, DOS_QM, DOS_DOT): an + // asterisk matches a run of characters (but not past a dot that a following dot token still has to + // account for); a question mark matches one character that is not a dot, or nothing where the name has + // no more characters left or the next one is a dot; and a dot matches a literal dot in the name, or + // nothing where the name has no more characters left, which is why "Report.*" also returns "Report" and + // "Report." returns only the names without a dot. Comparisons are case-insensitive with the invariant + // culture, never the thread culture, matching the enumeration (see InvariantCultureScope). Every + // character other than an asterisk or a question mark is ordinary, including a bracket and a backtick. + private static class DosWildcard + { + private enum TokenKind + { + Literal, + Star, + QuestionMark, + Dot + } + + private struct Token + { + public TokenKind Kind; + public char Char; + } + + public static bool IsMatch(string name, string pattern) + { + // An empty value matches every item, like a plain asterisk; Get-ChildItem2 keeps its own default + // of "*" for -Filter, but once a caller passes an explicit empty string this is the policy. + if (pattern.Length == 0) + { + return true; + } + + // A single dot is not a meaningful base/extension split, so Get-ChildItem treats it the same as + // no filter at all, in both editions. + if (pattern == ".") + { + return true; + } + + var tokens = Tokenize(pattern); + var matched = new Dictionary(); + return Match(name, 0, tokens, 0, matched); + } + + private static List Tokenize(string pattern) + { + var tokens = new List(); + + // A run of one or more dots that reaches the end of the pattern collapses into a single DOS_DOT: + // "Report..." and "Report." both ask for "Report" with nothing left of the name to account for. + // A dot elsewhere becomes a DOS_DOT only when the single next character is an asterisk or a + // question mark; otherwise it is an ordinary character that requires a literal dot in the name. + var trailingDotsFrom = pattern.Length; + while (trailingDotsFrom > 0 && pattern[trailingDotsFrom - 1] == '.') + { + trailingDotsFrom--; + } + var hasTrailingDotRun = trailingDotsFrom < pattern.Length; + + for (var i = 0; i < pattern.Length; i++) + { + if (hasTrailingDotRun && i == trailingDotsFrom) + { + tokens.Add(new Token { Kind = TokenKind.Dot, Char = '.' }); + break; + } + + var c = pattern[i]; + if (c == '*') + { + tokens.Add(new Token { Kind = TokenKind.Star }); + } + else if (c == '?') + { + tokens.Add(new Token { Kind = TokenKind.QuestionMark }); + } + else if (c == '.') + { + var next = (i + 1 < pattern.Length) ? pattern[i + 1] : '\0'; + var qualifies = (i + 1 >= pattern.Length) || next == '*' || next == '?'; + tokens.Add(new Token { Kind = qualifies ? TokenKind.Dot : TokenKind.Literal, Char = '.' }); + } + else + { + tokens.Add(new Token { Kind = TokenKind.Literal, Char = c }); + } + } + return tokens; + } + + // Matches the name, starting at name position ni, against the tokens from token position ti. The + // dictionary memoizes (ti, ni) pairs, because a star token tries every name position from ni onward + // and the same (ti, ni) pair can otherwise be re-examined many times over for a pattern with several + // stars. + private static bool Match(string name, int ni, List tokens, int ti, Dictionary matched) + { + var key = ((long)ti << 32) | (uint)ni; + bool cached; + if (matched.TryGetValue(key, out cached)) + { + return cached; + } + + var result = MatchCore(name, ni, tokens, ti, matched); + matched[key] = result; + return result; + } + + private static bool MatchCore(string name, int ni, List tokens, int ti, Dictionary matched) + { + if (ti == tokens.Count) + { + return ni == name.Length; + } + + var tok = tokens[ti]; + + if (tok.Kind == TokenKind.Star) + { + // A star that is directly followed by a dot (a DOS_DOT or an ordinary dot) must leave that dot + // for the dot token to account for: it cannot run past the next dot in the name, which is why + // "Rep*." does not return "Report.txt" although a plain "*" could reach the end of the name by + // crossing the dot. A star with anything else after it, including another star, is free to + // cross dots, which is why "**." returns every item and "R*t" returns "Report.txt". + var max = name.Length; + if (ti + 1 < tokens.Count && tokens[ti + 1].Kind == TokenKind.Dot) + { + var nextDot = name.IndexOf('.', ni); + if (nextDot >= 0) + { + max = nextDot; + } + } + + for (var k = ni; k <= max; k++) + { + if (Match(name, k, tokens, ti + 1, matched)) + { + return true; + } + } + return false; + } + + if (ni == name.Length) + { + // A question mark or a dot can match nothing at the end of the name; anything else needs a + // character that is not there. + if (tok.Kind == TokenKind.QuestionMark || tok.Kind == TokenKind.Dot) + { + return Match(name, ni, tokens, ti + 1, matched); + } + return false; + } + + if (tok.Kind == TokenKind.QuestionMark) + { + // A question mark does not match a dot; it matches nothing there instead, leaving the dot for + // a later token. + if (name[ni] == '.') + { + return Match(name, ni, tokens, ti + 1, matched); + } + return Match(name, ni + 1, tokens, ti + 1, matched); + } + + if (tok.Kind == TokenKind.Dot) + { + if (name[ni] == '.') + { + return Match(name, ni + 1, tokens, ti + 1, matched); + } + return false; + } + + if (char.ToUpperInvariant(tok.Char) == char.ToUpperInvariant(name[ni])) + { + return Match(name, ni + 1, tokens, ti + 1, matched); + } + + return false; + } + } } } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index e6a1e4a..8d8cb73 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -122,6 +122,23 @@ Describe 'Get-ChildItem2' { Assert-TestSandboxPath -Sandbox $sandbox -Path (@($grandchild) + @($paths)) New-Item -ItemType Directory -Path $grandchild -Force | Out-Null foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Tree' } + + # The fixture for the -Filter truth table: 18 files and 4 folders, all 8.3-compliant (at most one dot, + # up to 8 characters of base name and 3 of extension, no spaces), so each real name is also its own + # short name and AlphaFS enumerates the same identity that the matcher compares. + $filterTruthTableFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterTruthTable' -Directory + $truthTableFiles = 'a', 'ab', 'abc', 'a.b', 'ab.c', 'abc.de', 'a.bcd', 'abcdefgh', 'abcdefgh.ijk', 'x.y', 'a1.b2', 'bca', 'cab.cd', 'ccc', 'c.c', 'bcabcabc', 'a2.b1', '1cb' + $truthTableDirs = 'bdir', 'bdir.cd', 'fold', 'fo.ld' + foreach ($name in $truthTableFiles) { + $path = Join-Path -Path $filterTruthTableFolder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $path + Set-Content -LiteralPath $path -Value $name + } + foreach ($name in $truthTableDirs) { + $path = Join-Path -Path $filterTruthTableFolder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $path + New-Item -ItemType Directory -Path $path | Out-Null + } } It 'Should return the exact tree for ' -ForEach @( @@ -146,6 +163,9 @@ Describe 'Get-ChildItem2' { @{ Filter = '*.htm'; Expected = @('Page.htm') } @{ Filter = 'Page?.html'; Expected = @('Page2.html') } @{ Filter = 'PAGE*'; Expected = @('Page.htm', 'Page2.html') } + @{ Filter = 'Page.ht?'; Expected = @('Page.htm') } + @{ Filter = 'Page2.h*'; Expected = @('Page2.html') } + @{ Filter = 'Page2.'; Expected = @() } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory foreach ($name in 'Page.htm', 'Page2.html') { @@ -229,9 +249,12 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm' } - # The enumeration returns every item for *.*.*, as Get-ChildItem does. The cmdlet compares each name with the whole - # pattern again, so that the dots of the pattern are characters of the name, as the help says. - It 'Should return only the items that match the whole pattern for -Filter *.*.*' { + # The cmdlet now has one matcher, not two (Decision 27, item 4): every dot in -Filter whose single next + # character is an asterisk or a question mark, or that has nothing after it, is a DOS_DOT, and *.*.* has + # two such dots, so it returns every item, the same as *.* and a plain *. Before this change, the cmdlet + # compared the name with the pattern a second time with the dots read as ordinary characters, so only a + # name with two literal dots matched. + It 'Should return every item for -Filter *.*.*, also the ones with one dot or none' { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDots' -Directory foreach ($name in 'Page.htm', 'NoExtension', 'Two.dots.txt') { $file = Join-Path -Path $folder -ChildPath $name @@ -241,20 +264,20 @@ Describe 'Get-ChildItem2' { $result = @(Get-ChildItem2 -Path $folder -Filter '*.*.*' -ErrorAction Stop) - ($result.Name -join ',') | Should -BeExactly 'Two.dots.txt' + ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,Page.htm,Two.dots.txt' } - # The names are matched twice, by the enumeration and by the cmdlet, and the rules for a dot differ from those of - # Get-ChildItem, where Report.* also returns Report. The documentation lists this as a limitation; these cases pin - # it, so that a change of the rules is a decision. Report* is the control: without a dot in the pattern, the names - # without a dot are returned. + # The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 + # applies to Get-ChildItem -Filter, in both editions (Decision 27, item 4): Report.* also returns Report, + # which has no dot, and a trailing dot asks for the names without a dot. Report* is the control: without a + # dot in the pattern, the names with and without a dot are both returned, as before. It 'Should return for -Filter ""' -ForEach @( - @{ Filter = 'Report.*'; Expected = 'Report.txt'; Outcome = 'only the names with a dot' } + @{ Filter = 'Report.*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' } @{ Filter = 'Report*'; Expected = 'Report,Report.txt'; Outcome = 'the names with and without a dot' } - @{ Filter = 'Report.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = 'Rep*.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = '*.'; Expected = ''; Outcome = 'nothing' } - @{ Filter = ''; Expected = ''; Outcome = 'nothing' } + @{ Filter = 'Report.'; Expected = 'Report'; Outcome = 'only the name without a dot' } + @{ Filter = 'Rep*.'; Expected = 'Report'; Outcome = 'only the name without a dot' } + @{ Filter = '*.'; Expected = 'Other,Report'; Outcome = 'the names without a dot' } + @{ Filter = ''; Expected = 'Other,Report,Report.txt'; Outcome = 'every item, like no filter at all' } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterDotRules' -Directory foreach ($name in 'Report', 'Report.txt', 'Other') { @@ -268,6 +291,104 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly $Expected } + # A fixed truth table, pinned with the literal result of Windows PowerShell 5.1's Get-ChildItem on 8.3-compliant + # names (at most one dot, up to 8 characters of base name and 3 of extension, no spaces), whose short name is + # its own real name, so AlphaFS and the cmdlet agree on which name they compare. The expected values are data, + # not the output of a run of Get-ChildItem: this test is its own oracle. + It 'Should match -Filter "" the way Windows PowerShell 5.1 does' -ForEach @( + @{ Filter = '*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '*.'; Expected = @('1cb', 'a', 'ab', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'ccc', 'fold') } + @{ Filter = 'a.*'; Expected = @('a', 'a.b', 'a.bcd') } + @{ Filter = 'ab.*'; Expected = @('ab', 'ab.c') } + @{ Filter = 'a.'; Expected = @('a') } + @{ Filter = 'ab.'; Expected = @('ab') } + @{ Filter = 'a*.'; Expected = @('a', 'ab', 'abc', 'abcdefgh') } + @{ Filter = 'ab*.'; Expected = @('ab', 'abc', 'abcdefgh') } + @{ Filter = 'c*.'; Expected = @('ccc') } + @{ Filter = '*.b'; Expected = @('a.b') } + @{ Filter = '*.c?'; Expected = @('ab.c', 'bdir.cd', 'c.c', 'cab.cd') } + @{ Filter = '*.b?'; Expected = @('a.b', 'a1.b2', 'a2.b1') } + @{ Filter = '*.?'; Expected = @('1cb', 'a', 'a.b', 'ab', 'ab.c', 'abc', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'c.c', 'ccc', 'fold', 'x.y') } + @{ Filter = '*.??'; Expected = @('1cb', 'a', 'a.b', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '?'; Expected = @('a') } + @{ Filter = '??'; Expected = @('a', 'ab') } + @{ Filter = 'a?'; Expected = @('a', 'ab') } + @{ Filter = 'b?'; Expected = @() } + @{ Filter = 'ab?'; Expected = @('ab', 'abc') } + @{ Filter = 'a?.'; Expected = @('a', 'ab') } + @{ Filter = 'bc?'; Expected = @('bca') } + @{ Filter = '?.?'; Expected = @('a', 'a.b', 'c.c', 'x.y') } + @{ Filter = 'c.?'; Expected = @('c.c') } + @{ Filter = '*a*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'cab.cd') } + @{ Filter = '*c*'; Expected = @('1cb', 'a.bcd', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir.cd', 'c.c', 'cab.cd', 'ccc') } + @{ Filter = 'a*.*'; Expected = @('a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk') } + @{ Filter = 'bca*.'; Expected = @('bca', 'bcabcabc') } + @{ Filter = '*.*.*'; Expected = @('1cb', 'a', 'a.b', 'a.bcd', 'a1.b2', 'a2.b1', 'ab', 'ab.c', 'abc', 'abc.de', 'abcdefgh', 'abcdefgh.ijk', 'bca', 'bcabcabc', 'bdir', 'bdir.cd', 'c.c', 'cab.cd', 'ccc', 'fo.ld', 'fold', 'x.y') } + @{ Filter = '.*'; Expected = @() } + @{ Filter = '*..*'; Expected = @() } + @{ Filter = 'abc'; Expected = @('abc') } + @{ Filter = 'cba'; Expected = @() } + @{ Filter = 'a.b.'; Expected = @('a.b') } + @{ Filter = 'ab.c.'; Expected = @('ab.c') } + @{ Filter = '*b.'; Expected = @('1cb', 'a.b', 'ab') } + @{ Filter = 'cab*'; Expected = @('cab.cd') } + @{ Filter = 'a??'; Expected = @('a', 'ab', 'abc') } + @{ Filter = 'c.c'; Expected = @('c.c') } + @{ Filter = 'ca?'; Expected = @() } + ) { + $folder = $filterTruthTableFolder + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') + } + + It 'Should apply a -Filter at every level of a recursive listing, directories included' { + $recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory + $paths = @( + 'A.txt', 'B.log', + 'Sub\C.txt', 'Sub\D.log', + 'Sub\Sub2\E.txt', 'Sub\Sub2\F.log' + ) | ForEach-Object -Process { Join-Path -Path $recurseFolder -ChildPath $_ } + Assert-TestSandboxPath -Sandbox $sandbox -Path $paths + New-Item -ItemType Directory -Path (Join-Path -Path $recurseFolder -ChildPath 'Sub\Sub2') -Force | Out-Null + foreach ($path in $paths) { Set-Content -LiteralPath $path -Value 'Content' } + + $result = @(Get-ChildItem2 -Path $recurseFolder -Recurse -Filter '*.txt' -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'A.txt,C.txt,E.txt' + } + + It 'Should apply a dot -Filter together with -File and with -Directory' { + $folder = $filterTruthTableFolder + + $files = @(Get-ChildItem2 -Path $folder -File -Filter '*.' -Force -ErrorAction Stop) + $directories = @(Get-ChildItem2 -Path $folder -Directory -Filter '*.' -Force -ErrorAction Stop) + + ($files.Name | Sort-Object) -join ',' | Should -BeExactly '1cb,a,ab,abc,abcdefgh,bca,bcabcabc,ccc' + ($directories.Name | Sort-Object) -join ',' | Should -BeExactly 'bdir,fold' + } + + It 'Should return every item for an empty -Filter, like no filter at all' { + $folder = $filterTruthTableFolder + + $result = @(Get-ChildItem2 -Path $folder -Filter '' -Force -ErrorAction Stop) + + $result | Should -HaveCount 22 + } + + It 'Should apply -Filter to a -Path that is a file' { + $file = Join-Path -Path $tree -ChildPath 'Root.txt' + + $matching = @(Get-ChildItem2 -Path $file -Filter '*.txt' -ErrorAction Stop) + $other = @(Get-ChildItem2 -Path $file -Filter '*.log' -ErrorAction Stop) + + $matching | Should -HaveCount 1 + $matching[0].FullName | Should -Be $file + $other | Should -BeNullOrEmpty + } + It 'Should reject a null -Filter' { { Get-ChildItem2 -Path $tree -Filter $null -ErrorAction Stop } | Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*" From ab2fb87f1f331021694837ea63f47c8f350e600b Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:04:12 +0000 Subject: [PATCH 08/36] fix(items): let an asterisk before a dot in -Filter cross every dot but the last An independent review found that the matcher of Get-ChildItem2 -Filter (commit 69cd8ed) bounded a star in front of a dot token at the FIRST dot of the name instead of the LAST one (Win32 DOS_STAR). The differential oracle of 69cd8ed used only 8.3-compliant names, which have at most one dot, so the first dot and the last dot are always the same position there and the bug could not show up. With a name that has two or more dots, such as "archive.tar.gz" or "jquery.min.js", the star stopped too early and the rest of the pattern could not reach the final segment of the name, so a pattern such as "*.?", "*.??", or "*.?.?" wrongly returned nothing for these names, although Windows PowerShell 5.1 returns them. What a user sees changing: -Filter patterns of the shape "a star followed by a dot" (including "*.?", "*.??", "*.???", a star in a multi-part pattern such as "a*.?" or "x.*.?") now also match names with two or more dots, the same as Windows PowerShell 5.1, in both editions. A name with at most one dot is unaffected (that case was already correct). Fixed by bounding the star at `name.LastIndexOf('.')` instead of the first dot at or after the current position, and corrected the two comments that described the old, narrower rule. Also added a discriminating row to the existing short-name test (`-Filter 'LONGFI~1*'` against a plain file named `LongFileName`, expected to return nothing): the three rows already in that test happen to give the same result whether the matcher reads the real name or the short name, so they could not have caught a regression to short-name matching; this row can. Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type filters, and depth"): added a 9-row test with 10 names that have two or three dots (including the realistic ones from the review: archive.tar.gz, jquery.min.js, my.file.name.txt, v1.2.3, index.d.ts), expected values pinned from Windows PowerShell 5.1 with `fsutil file setshortname` (and, for one row, `fsutil 8dot3name strip`) so that a short name the volume assigns cannot also produce a match; and the LONGFI~1* row above. Both fail on the build of 69cd8ed and pass after this fix. The fix was checked with the reviewer's differential harness (C:\Temp\ntfs-rc9\review-probes-r3\diff5.ps1) against this build: 0 mismatches over two independent fixtures (732,630 real-name/pattern pairs with names of two or more dots, short names overridden with `fsutil file setshortname` so that a hit can only come from the real name), in addition to the 109,308 pairs of 8.3-compliant names checked for 69cd8ed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 31 +++++++++++---------- Tests/ItemCmdlets.Tests.ps1 | 34 ++++++++++++++++++++++- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 313d6d9..39d8b21 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -401,13 +401,14 @@ namespace NTFSSecurity // Windows PowerShell 5.1 applies to Get-ChildItem -Filter, in both editions, so that the two editions and // the short-name based matching that a past fix removed do not disagree. The rules come from // FsRtlIsNameInExpression with the Win32 translation of a DOS pattern (DOS_STAR, DOS_QM, DOS_DOT): an - // asterisk matches a run of characters (but not past a dot that a following dot token still has to - // account for); a question mark matches one character that is not a dot, or nothing where the name has - // no more characters left or the next one is a dot; and a dot matches a literal dot in the name, or - // nothing where the name has no more characters left, which is why "Report.*" also returns "Report" and - // "Report." returns only the names without a dot. Comparisons are case-insensitive with the invariant - // culture, never the thread culture, matching the enumeration (see InvariantCultureScope). Every - // character other than an asterisk or a question mark is ordinary, including a bracket and a backtick. + // asterisk matches a run of characters (but not past the LAST dot in the name, which a following dot + // token still has to account for); a question mark matches one character that is not a dot, or nothing + // where the name has no more characters left or the next one is a dot; and a dot matches a literal dot in + // the name, or nothing where the name has no more characters left, which is why "Report.*" also returns + // "Report" and "Report." returns only the names without a dot. Comparisons are case-insensitive with the + // invariant culture, never the thread culture, matching the enumeration (see InvariantCultureScope). + // Every character other than an asterisk or a question mark is ordinary, including a bracket and a + // backtick. private static class DosWildcard { private enum TokenKind @@ -521,17 +522,19 @@ namespace NTFSSecurity if (tok.Kind == TokenKind.Star) { // A star that is directly followed by a dot (a DOS_DOT or an ordinary dot) must leave that dot - // for the dot token to account for: it cannot run past the next dot in the name, which is why - // "Rep*." does not return "Report.txt" although a plain "*" could reach the end of the name by - // crossing the dot. A star with anything else after it, including another star, is free to - // cross dots, which is why "**." returns every item and "R*t" returns "Report.txt". + // for the dot token to account for: it may cross every dot in the name but the LAST one (Win32 + // DOS_STAR), which is why "Rep*." does not return "Report.txt" although a plain "*" could reach + // the end of the name by crossing the dot, and why "*.?" reaches the final segment of a name + // with two or more dots, such as "archive.tar.gz". A star with anything else after it, + // including another star, is free to cross every dot, which is why "**." returns every item + // and "R*t" returns "Report.txt". var max = name.Length; if (ti + 1 < tokens.Count && tokens[ti + 1].Kind == TokenKind.Dot) { - var nextDot = name.IndexOf('.', ni); - if (nextDot >= 0) + var lastDot = name.LastIndexOf('.'); + if (lastDot >= ni) { - max = nextDot; + max = lastDot; } } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 8d8cb73..9a15360 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -166,9 +166,10 @@ Describe 'Get-ChildItem2' { @{ Filter = 'Page.ht?'; Expected = @('Page.htm') } @{ Filter = 'Page2.h*'; Expected = @('Page2.html') } @{ Filter = 'Page2.'; Expected = @() } + @{ Filter = 'LONGFI~1*'; Expected = @() } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory - foreach ($name in 'Page.htm', 'Page2.html') { + foreach ($name in 'Page.htm', 'Page2.html', 'LongFileName') { $file = Join-Path -Path $folder -ChildPath $name Assert-TestSandboxPath -Sandbox $sandbox -Path $file Set-Content -LiteralPath $file -Value $name @@ -344,6 +345,37 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') } + # A star in front of a dot token may cross every dot in the name but the LAST one (Win32 DOS_STAR), not stop + # at the first one: with names that have two or more dots, a pattern such as "*.?" still has to reach the + # last dot before the dot token and the question mark account for the final segment. The 8.3-compliant + # names of the truth table above have at most one dot, where the first dot and the last dot are the same + # position, so they cannot tell the two rules apart; this fixture uses names with two or three dots + # instead, including the realistic ones from the WI. Expected values are pinned with the real name of each + # item, confirmed with `fsutil file setshortname` so that a short name the volume might otherwise assign + # cannot also produce a match. + It 'Should match -Filter "" on a name with two or more dots the way Windows PowerShell 5.1 does' -ForEach @( + @{ Filter = '*.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.??'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.???'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.?.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } + @{ Filter = '*.*.*'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.name.txt', 'my.file.tx', 'one.two.three', 'v1.2.3', 'x.y.z.w') } + @{ Filter = 'a*.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.?'; Expected = @('a.b.c') } + @{ Filter = 'a.b.??'; Expected = @('a.b.c', 'a.b.cd') } + @{ Filter = 'x.*.?'; Expected = @('x.y.z.w') } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMultiDot' -Directory + foreach ($name in 'a.b.c', 'a.b.cd', 'x.y.z.w', 'my.file.tx', 'one.two.three', 'archive.tar.gz', 'jquery.min.js', 'my.file.name.txt', 'v1.2.3', 'index.d.ts') { + $file = Join-Path -Path $folder -ChildPath $name + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value $name + } + + $result = @(Get-ChildItem2 -Path $folder -Filter $Filter -Force -ErrorAction Stop) + + ($result.Name | Sort-Object) -join ',' | Should -BeExactly (($Expected | Sort-Object) -join ',') + } + It 'Should apply a -Filter at every level of a recursive listing, directories included' { $recurseFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterRecurse' -Directory $paths = @( From 2deec1761b903fc88eaff577f16917af9cc85db2 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:05:51 +0000 Subject: [PATCH 09/36] fix(items): reject a -Filter of more than 32,767 characters An independent review found that a very long -Filter pattern (for example, a run of 120,000 question marks, or 45,000 asterisks) crashes the whole host process with an uncatchable StackOverflowException: the matcher recurses roughly once per character of the pattern, and a long enough pattern exhausts the stack. A pattern this long serves no purpose that a shorter one could not also serve, and the review confirmed that patterns of 32,767 characters or fewer, of any shape (literal, "?" repeated, "*" repeated, "*?" repeated, "*." repeated), do not come close to exhausting the stack. Added `[ValidateLength(0, 32767)]` next to the existing `[ValidateNotNull]` on -Filter, so that a pattern longer than 32,767 characters is rejected at parameter binding, before it ever reaches the matcher, with the same `ParameterArgumentValidationError` and `'Filter'` in the message as the existing null rejection; shorter values, including an empty one, are unaffected. Tests (Tests\ItemCmdlets.Tests.ps1, "Get-ChildItem2" > "Recursion, type filters, and depth"): a -Filter of 32,768 characters now gives ParameterArgumentValidationError, and one of exactly 32,767 characters ("?" repeated, in a folder with one file) returns the right result without crashing. The crash itself was confirmed in a disposable child process (not through the shared Pester harness, to avoid taking that process down too) with the reviewer's probe (C:\Temp\ntfs-rc9\review-probes-r3\p9-longpattern.ps1 -N 120000 -Kind qm): the process terminated with a StackOverflowException before this fix, and after it, the same probe at the exact boundaries (32,767 and 32,768 characters, every pattern kind the probe offers) neither crashes nor is accepted past the limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ItemCmdlets/GetChildItem2.cs | 1 + Tests/ItemCmdlets.Tests.ps1 | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs index 39d8b21..fd66987 100644 --- a/NTFSSecurity/ItemCmdlets/GetChildItem2.cs +++ b/NTFSSecurity/ItemCmdlets/GetChildItem2.cs @@ -44,6 +44,7 @@ namespace NTFSSecurity [Parameter(Position = 2)] [ValidateNotNull] + [ValidateLength(0, 32767)] public string Filter { get { return filter; } diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 9a15360..5c26da8 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -426,6 +426,28 @@ Describe 'Get-ChildItem2' { Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*" } + It 'Should reject a -Filter of more than 32,767 characters' { + $tooLong = '?' * 32768 + { Get-ChildItem2 -Path $tree -Filter $tooLong -ErrorAction Stop } | + Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*" + } + + # A pattern this long is not useful, but the cmdlet must not crash the host: before this test was added, a + # pattern of this length (or a long run of asterisks) recursed deep enough in the matcher to raise an + # uncatchable StackOverflowException and terminate the process. + It 'Should accept a -Filter of exactly 32,767 characters and return the right result' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterMaxLength' -Directory + $file = Join-Path -Path $folder -ChildPath 'a' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + Set-Content -LiteralPath $file -Value 'a' + $longest = '?' * 32767 + + $result = @(Get-ChildItem2 -Path $folder -Filter $longest -ErrorAction Stop) + + $result | Should -HaveCount 1 + $result[0].Name | Should -Be 'a' + } + It 'Should stop a recursive pipeline without recording an enumeration error' { $result = @(Get-ChildItem2 -Path $tree -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 1) From dbd671763b892c95a11fb01f435ccacf11dfe587 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:08:43 +0000 Subject: [PATCH 10/36] docs(items): split -Filter paragraphs for platyPS, sharpen the PowerShell 7 claim, fix -Path wording An independent review found that platyPS 0.14.2, which generates the shipped help file from these pages, pairs up every two literal asterisks within one paragraph as markdown emphasis and drops them, even inside backticks: the -Filter parameter description and the matching paragraph of NOTES each had three or four literal asterisks in one paragraph, so `Get-Help Get-ChildItem2 -Parameter Filter` and the NOTES section showed `Report. ` instead of `` Report.* `` and ` . ` instead of a plain asterisk pattern, silently losing the character that the sentence was about. Both paragraphs are now several short paragraphs, each with at most one literal asterisk; a pattern that itself needs more than one asterisk, such as a plain asterisk, a dot, and an asterisk, is spelled out in words instead, the way the page already did before this feature. Verified by running `New-ExternalHelp` from platyPS 0.14.2 into a temporary folder (not the repository's help file) and confirming every pattern renders intact in the generated XML, then removing the folder. The same two paragraphs also said PowerShell 7's own `Get-ChildItem -Filter` returns nothing for any pattern that ends in a dot and returns every item only for a pattern of an asterisk, a dot, and one question mark. Measured directly on PowerShell 7.6.7, both overstated the difference: a trailing dot returns nothing there only when the character right before the dot is not an asterisk (a star right before the dot behaves the same as Windows PowerShell 5.1 and as Get-ChildItem2 there too), and a pattern of an asterisk, a dot, and two question marks also returns every item, not only one question mark. Both paragraphs now say exactly that. Also removed "(Decision 27, item 4)" from both paragraphs and from two test comments (the maintainer's agent already removed it from the integration branch; this avoids a conflicting edit), and changed one sentence of the DESCRIPTION section that said every -Path value must name a folder: a value that names a file returns that file, as the PARAMETERS section and NOTES already said and the cmdlet already did. No test changes: these are documentation wording changes only, and the page keeps its existing structure (headings, parameter block, NOTES list). Re-ran Tests\ItemCmdlets.Tests.ps1 (no code changed in this commit) to confirm the suite is still green: elevated-Desktop 193/0/0, elevated-Core 192/0/1, basic-Desktop 187/0/6, basic-Core 186/0/7 (passed/failed/skipped), three times, identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Get-ChildItem2.md | 18 +++++++++++++++--- Tests/ItemCmdlets.Tests.ps1 | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index dccbb2e..2db61e7 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -23,7 +23,7 @@ Get-ChildItem2 [[-Path] ] [[-Filter] ] [-Recurse] [-Directory] The `Get-ChildItem2` cmdlet lists the files and folders in the folders that you specify with `-Path`. It returns an `Alphaleonis.Win32.Filesystem.FileInfo` object for every file and an `Alphaleonis.Win32.Filesystem.DirectoryInfo` object for every folder. The cmdlet is the long-path counterpart of the built-in `Get-ChildItem` cmdlet: it enumerates the file system through the AlphaFS library (`Alphaleonis.Win32.Filesystem`) instead of `System.IO`, so it also returns items whose path is longer than the 260-character `MAX_PATH` limit. -If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name a folder, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. +If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name an existing folder or file, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. By default the cmdlet returns the immediate content of each folder and omits hidden items. Use `-Recurse` to walk the whole tree, `-Depth` to limit how deep the recursion goes, `-Filter` to restrict the result by name, `-Directory` or `-File` to restrict it by item type, and `-Force`, `-Hidden`, `-System`, `-ReadOnly`, or `-Attributes` to restrict it by file attributes. @@ -142,7 +142,15 @@ Accept wildcard characters: False ### -Filter -Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name: an asterisk matches a run of characters, a question mark matches one character that is not a dot (nothing where a dot or the end of the name follows), and a dot also matches nothing where only wildcards or the end of the pattern follow, so that `Report.*` also returns `Report`, which has no dot, and `*.*` returns every item. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. PowerShell 7 deliberately differs here from the built-in `Get-ChildItem`, whose own `-Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`; `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. +Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + +For example, `Report.*` also returns the item `Report`, which has no dot. + +A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + +PowerShell 7 deliberately differs here from the built-in `Get-ChildItem`: its own `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead, so these are deliberate differences from PowerShell 7's own `Get-ChildItem`. + +The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. ```yaml Type: String @@ -319,7 +327,11 @@ Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an ` Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. -The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions (Decision 27, item 4): `Report.*` also returns `Report`, which has no dot, a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, like `*`. PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot and returns every item for `*.?`. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these two pattern shapes. +The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. + +For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. + +PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these pattern shapes. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index 5c26da8..e610a82 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -250,7 +250,7 @@ Describe 'Get-ChildItem2' { ($result.Name | Sort-Object) -join ',' | Should -BeExactly 'NoExtension,NoExtensionFolder,Page.htm' } - # The cmdlet now has one matcher, not two (Decision 27, item 4): every dot in -Filter whose single next + # The cmdlet now has one matcher, not two: every dot in -Filter whose single next # character is an asterisk or a question mark, or that has nothing after it, is a DOS_DOT, and *.*.* has # two such dots, so it returns every item, the same as *.* and a plain *. Before this change, the cmdlet # compared the name with the pattern a second time with the dots read as ordinary characters, so only a @@ -269,7 +269,7 @@ Describe 'Get-ChildItem2' { } # The names are matched once, by the cmdlet, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 - # applies to Get-ChildItem -Filter, in both editions (Decision 27, item 4): Report.* also returns Report, + # applies to Get-ChildItem -Filter, in both editions: Report.* also returns Report, # which has no dot, and a trailing dot asks for the names without a dot. Report* is the control: without a # dot in the pattern, the names with and without a dot are both returned, as before. It 'Should return for -Filter ""' -ForEach @( From 62cf026e6e206f6db1f564985ca7c987998e11f2 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:14:31 +0000 Subject: [PATCH 11/36] fix(owner): pass on what a later command raises at the RestoreOwnerError record InvokeAsOwner (and Set-NTFSSecurityDescriptor's WriteChangesAsOwner) take ownership of an item, run the action, and in a finally restore the previous owner, writing a non-terminating RestoreOwnerError through WriteError when that restore fails. WriteError runs the later commands of the pipeline while it writes, so in the rare case that a later command throws exactly when it sees that RestoreOwnerError record (for example `... 2>&1 | ForEach-Object { if ($_.FullyQualifiedErrorId -like 'RestoreOwnerError,*') { throw } else { $_ } }`), that exception used to leave InvokeAsOwner through the finally and reach the handler around the call, which reported it as the item's own error (AddAceError, ReadSecurityError, ModifySdError, and so on) and moved on to the next path. The caller never saw the exception. Thirteen handlers now check IsFromLaterCommand as the first statement of their catch, the same pattern SetSecurityDescriptor.cs already used in its second handler and GetSecurityDescriptor.cs already used: Add/Clear and Get-NTFSAccess, Get-NTFSOrphanedAccess, Get-NTFSEffectiveAccess, Clear-NTFSAudit, Get-FileHash2, the six inheritance cmdlets, and the owner-retry of Set-NTFSSecurityDescriptor. The four add/remove access and audit cmdlets have the same flaw and belong to another work item. Tests\PipelineControl.Tests.ps1 adds one table-driven It, elevated only, that denies the current user the right a cmdlet's action needs, gives the item an owner only the Restore privilege can set back (which the test turns off), and asserts that a later command's exception reaches the caller, no item-error record was written for it, and the next path is untouched. Six of the thirteen handlers are pinned this way (the other seven need the current user to both fail and succeed the same access check at once, which Windows does not allow; see the comment above the Describe block and the probes under ..\probes-wi2\ for the empirical evidence). The four add and remove cmdlets, which have the same flaw, follow in the next commit of this item. Get-NTFSInheritance, the one of the thirteen that writes its result in a finally, writes it after the try instead, so that the object does not go to the later command that has just thrown. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/AccessCmdlets/ClearAccess.cs | 6 + NTFSSecurity/AccessCmdlets/GetAccess.cs | 6 + .../AccessCmdlets/GetEffectiveAccess.cs | 6 + .../AccessCmdlets/GetOrphanedAccess.cs | 6 + NTFSSecurity/AuditCmdlets/ClearAudit.cs | 6 + .../DisableAccessInheritance.cs | 6 + .../DisableAuditInheritance.cs | 6 + .../EnableAccessInheritance.cs | 6 + .../EnableAuditInheritance.cs | 6 + .../InheritanceCmdlets/GetInheritance.cs | 16 +- .../InheritanceCmdlets/SetInheritance.cs | 6 + NTFSSecurity/MiscCmdlets/GetFileHash2.cs | 6 + .../SetSecurityDescriptor.cs | 6 + Tests/PipelineControl.Tests.ps1 | 161 ++++++++++++++++++ 14 files changed, 244 insertions(+), 5 deletions(-) diff --git a/NTFSSecurity/AccessCmdlets/ClearAccess.cs b/NTFSSecurity/AccessCmdlets/ClearAccess.cs index 1e4a1c6..28cb1bb 100644 --- a/NTFSSecurity/AccessCmdlets/ClearAccess.cs +++ b/NTFSSecurity/AccessCmdlets/ClearAccess.cs @@ -86,6 +86,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ClearAclError", ErrorCategory.WriteError, path)); } } diff --git a/NTFSSecurity/AccessCmdlets/GetAccess.cs b/NTFSSecurity/AccessCmdlets/GetAccess.cs index 95af170..fc7e7ce 100644 --- a/NTFSSecurity/AccessCmdlets/GetAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetAccess.cs @@ -112,6 +112,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs index 01ab384..5104efa 100644 --- a/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs @@ -141,6 +141,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + this.WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs index 14ecb9d..48c85e7 100644 --- a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs @@ -55,6 +55,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + // A read error; before 5.0.0-rc6, it was reported as an AddAceError. this.WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.ReadError, path)); continue; diff --git a/NTFSSecurity/AuditCmdlets/ClearAudit.cs b/NTFSSecurity/AuditCmdlets/ClearAudit.cs index 88aef1e..c5ce3ec 100644 --- a/NTFSSecurity/AuditCmdlets/ClearAudit.cs +++ b/NTFSSecurity/AuditCmdlets/ClearAudit.cs @@ -86,6 +86,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ClearAclError", ErrorCategory.WriteError, path)); } } diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs index bdb964d..56b3e89 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs @@ -90,6 +90,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs index 6505e1c..46c4cda 100644 --- a/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs @@ -96,6 +96,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs index 16ea497..1e6c7d7 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs @@ -90,6 +90,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs index 0c1a362..ce3d89e 100644 --- a/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs @@ -95,6 +95,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs index 9b02228..2a21f29 100644 --- a/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/GetInheritance.cs @@ -80,6 +80,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path)); continue; } @@ -89,12 +95,12 @@ namespace NTFSSecurity WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); continue; } - finally + + // After the try, not in a finally: what a later command raises while the retry restores the owner + // leaves the loop through the check above, and the object must not go to that command then. + if (inheritanceInfo != null) { - if (inheritanceInfo != null) - { - WriteObject(inheritanceInfo); - } + WriteObject(inheritanceInfo); } } } diff --git a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs index 16ef6e6..893d202 100644 --- a/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs +++ b/NTFSSecurity/InheritanceCmdlets/SetInheritance.cs @@ -98,6 +98,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "ModifySdError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs index 74e07d9..667715e 100644 --- a/NTFSSecurity/MiscCmdlets/GetFileHash2.cs +++ b/NTFSSecurity/MiscCmdlets/GetFileHash2.cs @@ -103,6 +103,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "GetHashError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs index 86b4a83..48bb106 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs @@ -61,6 +61,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "WriteSdError", ErrorCategory.WriteError, sd.Item)); continue; } diff --git a/Tests/PipelineControl.Tests.ps1 b/Tests/PipelineControl.Tests.ps1 index 96a035c..89f97bd 100644 --- a/Tests/PipelineControl.Tests.ps1 +++ b/Tests/PipelineControl.Tests.ps1 @@ -636,3 +636,164 @@ namespace NtfsSecurityTests { public class StandInForBreak : System.Management.A $isEnd.Invoke($null, @($Exception)) | Should -BeFalse } } + +# InvokeAsOwner (and Set-NTFSSecurityDescriptor's WriteChangesAsOwner) take ownership, run the action, and in a +# finally restore the previous owner, writing a RestoreOwnerError through WriteError when that restore fails. +# WriteError runs the later commands of the pipeline and notes what they raise (IsFromLaterCommand, above); the catch +# around the InvokeAsOwner/WriteChangesAsOwner call must pass that on instead of reporting it as the item's own error +# and going on, or the caller never sees it. Each row denies the current user the right its own action needs (a +# change denial for the writers, a read denial on Everyone for Get-FileHash2, which needs file data, not +# permissions) and gives the item an owner that only the Restore privilege can set back, which the tests turn off; +# InvokeAsOwner/WriteChangesAsOwner then takes ownership, runs the action, and fails to restore the owner. Six of +# the thirteen handlers of item 7 can be pinned this way; the scenario and the "without a later command" assertion +# already exist for all six: ParameterSets.Inheritance.Tests.ps1 (Disable/Enable-NTFSAccessInheritance, +# Set-NTFSInheritance), PathErrors.Tests.ps1 (Clear-NTFSAccess -DisableInheritance), SecurityDescriptor.Tests.ps1 +# (Set-NTFSSecurityDescriptor), FileHash.Tests.ps1 (Get-FileHash2), so this table only adds the later-command +# assertion, not a duplicate of the existing one. +# +# NOT DONE for the other seven (the reason is the same Windows rule in both groups: taking ownership only bypasses a +# denial of ReadPermissions/ChangePermissions, never of a different right): Get-NTFSAccess, Get-NTFSOrphanedAccess, +# Get-NTFSEffectiveAccess, and Get-NTFSInheritance read the security descriptor; InvokeAsOwner's own first statement, +# FileSystemOwner.GetOwner, reads it too and needs the same ReadPermissions right the cmdlet's read needs, so a +# denial that fails one fails the other and InvokeAsOwner never reaches the owner-restore step (verified: denying +# ReadPermissions directly to the user reports ReadSecurityError with the plain UnauthorizedAccessException, not +# RestoreOwnerError, and the owner is never taken; probes-wi2\p-restoreowner-read-vs-write.ps1, +# p-read-cmdlets-2.ps1). Clear-NTFSAudit, Disable-NTFSAuditInheritance, and Enable-NTFSAuditInheritance write the +# SACL, which Windows grants to this elevated session regardless of a DACL deny and regardless of the module's +# EnablePrivileges setting, so the action does not fail to begin with (verified with both a ChangePermissions deny +# and EnablePrivileges off: no error, the SACL is written, and the owner is left unchanged; +# probes-wi2\p-audit-restoreowner.ps1, p-audit-restoreowner-2.ps1). The integrator's four rows for +# Add/Remove-NTFSAccess/Audit need only the same four pieces of data (Prepare, Run, ItemErrorId, Untouched). +Describe 'A later command that throws at the RestoreOwnerError record' { + BeforeDiscovery { + $holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' + $laterCommandRestoreOwnerNames = @( + 'Clear-NTFSAccess', 'Disable-NTFSAccessInheritance', 'Enable-NTFSAccessInheritance', 'Set-NTFSInheritance', + 'Set-NTFSSecurityDescriptor', 'Get-FileHash2' + ) + } + + BeforeAll { + # An owner that the user can assign only with the Restore privilege, which EnablePrivileges = $false below + # keeps disabled, matching the existing RestoreOwnerError tests of the other files. + $trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464' + $enablePrivileges = $privateData['EnablePrivileges'] + $privateData['EnablePrivileges'] = $false + + $laterCommandRestoreOwnerCases = @{ + 'Clear-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterClearBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterClearNext' + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid 'S-1-5-32-544' + Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Clear-NTFSAccess -Path $Context.Blocked, $Context.Next -DisableInheritance -ErrorAction Continue } + ItemErrorId = 'ClearAclError' + Untouched = { param ($Context, $Emitted) (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Disable-NTFSAccessInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterDisableBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterDisableNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Disable-NTFSAccessInheritance -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Enable-NTFSAccessInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterEnableBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterEnableNext' + Disable-NTFSAccessInheritance -Path $blocked, $next -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Enable-NTFSAccessInheritance -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Set-NTFSInheritance' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSetInheritanceBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSetInheritanceNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Set-NTFSInheritance -Path $Context.Blocked, $Context.Next -AccessInheritanceEnabled $false -ErrorAction Continue } + ItemErrorId = 'ModifySdError' + Untouched = { param ($Context, $Emitted) -not (Get-Acl -LiteralPath $Context.Next).AreAccessRulesProtected -and $Emitted.Count -eq 0 } + } + 'Set-NTFSSecurityDescriptor' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSdBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterSdNext' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $blocked, $next -ErrorAction Stop) + Add-NTFSAccess -SecurityDescriptor $descriptors -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Descriptors = $descriptors; Next = $next } + } + Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -ErrorAction Continue } + ItemErrorId = 'WriteSdError' + Untouched = { param ($Context, $Emitted) -not (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } + 'Get-FileHash2' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterHashBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterHashNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ 'S-1-1-0' = 'ReadData' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Get-FileHash2 -Path $Context.Blocked, $Context.Next -ErrorAction Continue } + ItemErrorId = 'GetHashError' + # Get-FileHash2 has no ACL state of its own to read back, so the oracle is that the next path still + # exists, untouched, and that the pipeline produced no hash at all: the next path's hash would be the + # only object a later-command block ever sees, since neither -PassThru nor an output object exists + # for the blocked path in either state of the fix. Not @($Emitted): PowerShell's array operator + # raises ArgumentException for an empty generic List(Of T). + Untouched = { param ($Context, $Emitted) (Test-Path -LiteralPath $Context.Next) -and $Emitted.Count -eq 0 } + } + } + } + + AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + It '<_> should pass on what a later command raises at the RestoreOwnerError record instead of reporting it as the item''s own error, and leave the next path untouched' -Skip:(-not $holdsRestorePrivilege) -ForEach $laterCommandRestoreOwnerNames { + $case = $laterCommandRestoreOwnerCases[$_] + $context = & $case.Prepare + $Error.Clear() + $caught = $null + $emitted = New-Object -TypeName 'System.Collections.Generic.List[object]' + + try { + & $case.Run $context 2>&1 | ForEach-Object -Process { + if ($_ -is [System.Management.Automation.ErrorRecord] -and $_.FullyQualifiedErrorId -like 'RestoreOwnerError,*') { + throw 'later' + } + $emitted.Add($_) + $_ + } + } + catch { + $caught = $_ + } + + $caught.Exception.Message | Should -BeLike '*later*' + @($Error | Where-Object -FilterScript { $_.FullyQualifiedErrorId -like ('{0},*' -f $case.ItemErrorId) }) | Should -BeNullOrEmpty + (& $case.Untouched $context $emitted) | Should -BeTrue + } + + # Without a later command, the existing behavior stays: RestoreOwnerError is reported and the cmdlet goes on to + # the next path, already pinned by the tests named above for all six rows; not duplicated here. +} From cd788c76935fda73ae2e821a0f444b5ba8f321a7 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:13:51 +0000 Subject: [PATCH 12/36] test(access): pin the ReadSecurityError category of Get-NTFSAccess after a failed ownership retry Get-NTFSAccess writes ReadSecurityError with category WriteError when a read succeeds only after the cmdlet takes ownership and the restored owner is irrelevant to that path; Get-NTFSAudit, Get-NTFSEffectiveAccess, Get-NTFSInheritance, and Get-NTFSSecurityDescriptor already report the same failure the same way, so this is the module's convention, unlike Get-NTFSOrphanedAccess, whose own category for the same failure is ReadError. No script could notice: the behavior does not change. A characterization test in Tests\ParameterSets.Access.Tests.ps1, next to the orphaned-access test that already pins the ReadError side of the same convention, is green at once and runs in all four configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Tests/ParameterSets.Access.Tests.ps1 | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 551cc3b..bea7643 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -911,6 +911,37 @@ Describe 'Get-NTFSAccess' { @($piped | Where-Object -FilterScript { $_.FullName -eq $file2 }) | Should -HaveCount $expectedSecond } } + + Context 'Path parameter set, unreadable folder' { + BeforeAll { + # Forcing EnablePrivileges off reproduces the same denial as a basic user for an elevated session, as + # PathErrors.Tests.ps1 does for the same scenario, and as the Get-NTFSOrphanedAccess test below does. + $privateData['EnablePrivileges'] = $false + } + + AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + # Get-NTFSAccess, Get-NTFSAudit, Get-NTFSEffectiveAccess, Get-NTFSInheritance, and Get-NTFSSecurityDescriptor + # all report a failed ownership retry as ReadSecurityError categorized as WriteError (GetAccess.cs); this is + # the module's convention. A characterization test: the behavior is already correct, only not yet pinned for + # this cmdlet (6a). Unlike Get-NTFSOrphanedAccess, whose own category for the same failure is ReadError. + It 'Should write a ReadSecurityError categorized as WriteError after a failed ownership retry, and still return the next path''s entries' { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadable' + Block-TestReadPermission -Sandbox $sandbox -Path $blocked + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadableNext' + Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + + $result = @(Get-NTFSAccess -Path $blocked, $next -ErrorVariable accessErrors -ErrorAction SilentlyContinue) + + $accessErrors | Should -HaveCount 1 + $accessErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $accessErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $accessErrors[0].TargetObject | Should -Be $blocked + @($result | Where-Object -FilterScript { $_.FullName -eq $next -and $_.Account.Sid -eq 'S-1-1-0' -and -not $_.IsInherited }) | Should -HaveCount 1 + } + } } Describe 'Get-NTFSOrphanedAccess' { From 9fde8d19a49874d547685202ede8a3dae0f393f6 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:18:24 +0000 Subject: [PATCH 13/36] fix(access): write an error when Get-NTFSOrphanedAccess cannot read an item Get-NTFSOrphanedAccess wrote a warning ("Could not read item {0}. The error was: {1}") for a read failure that is not an access denial, while every sibling (Get-NTFSAccess, Get-NTFSAudit, Get-NTFSEffectiveAccess, Get-NTFSInheritance, Get-NTFSSecurityDescriptor) writes a non-terminating error and goes on with the next item. The cmdlet now writes ReadSecurityError, categorized as OpenError and targeting the path, through WriteError, then continues, the same pattern Get-NTFSAccess (GetAccess.cs) already uses for the same kind of failure. A script that collected this warning with -WarningVariable now has to read $Error or -ErrorVariable instead. No existing test pinned the warning (searched for "Could not read item" and Get-NTFSOrphanedAccess with -WarningVariable across Tests\; none found), so none needed changing. NOT DONE: a new test that triggers this exact catch (a read failure that is not UnauthorizedAccessException) through the public surface. Five probes (..\probes-wi2\p-orphaned-read-failure*.ps1) tried a sharing lock, an over-long path, a dangling and a looped junction, and an alternate data stream; none reached this catch without also failing GetFileSystemInfo2 first (a different error, ReadFileError) or succeeding outright, because an elevated session that can read the item at all can read its ACL as well. The change is still correct and matches the module's convention; it is defensive code for a condition this harness cannot construct deterministically. Docs\Cmdlets\Get-NTFSOrphanedAccess.md documents the new error next to the existing ReadSecurityError paragraph for the access-denied case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Get-NTFSOrphanedAccess.md | 2 ++ NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md index aa3b8d8..2aa63d2 100644 --- a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md +++ b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md @@ -174,6 +174,8 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. +A read failure that is not an access denial is reported the same way, as a non-terminating `ReadSecurityError`, categorized as `OpenError` and targeting the path; before 5.0.0-rc9, the cmdlet wrote a warning instead (`Could not read item {0}. The error was: {1}`) and returned no entry for the item, without an error to say why. + Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. ## RELATED LINKS diff --git a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs index 48c85e7..0ef9f49 100644 --- a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs @@ -68,7 +68,9 @@ namespace NTFSSecurity } catch (Exception ex) { - this.WriteWarning(string.Format("Could not read item {0}. The error was: {1}", path, ex.Message)); + // A read failure that is not an access denial; before 5.0.0-rc9, the cmdlet warned about it + // instead ("Could not read item {0}. The error was: {1}") and returned no entry for the item. + this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); continue; } From 37bd42d2ddaeef72273f443f34fc5948894fb98f Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:21:38 +0000 Subject: [PATCH 14/36] fix(securitydescriptor): make -SecurityDescriptor of Set-NTFSSecurityDescriptor the first positional parameter Set-NTFSSecurityDescriptor declared -SecurityDescriptor at Position = 2, its only positional parameter; every sibling that takes a security descriptor or a path takes its first positional parameter at 1. Moved to Position = 1 for consistency. A positional call with one argument already bound to it, because it was the only positional parameter in its only parameter set, so no script that calls the cmdlet positionally can notice a difference; this is cosmetic, as Decision 27 describes it. Tests\SecurityDescriptor.Tests.ps1 adds a characterization-turned-pin test: the declared Position is 1, and Set-NTFSSecurityDescriptor $sd still works positionally. RED before the change (Position was 2), GREEN after, in all four configurations. Docs\Cmdlets\Set-NTFSSecurityDescriptor.md: Position: 1 in the yaml block of -SecurityDescriptor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Set-NTFSSecurityDescriptor.md | 2 +- .../SetSecurityDescriptor.cs | 2 +- Tests/SecurityDescriptor.Tests.ps1 | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md b/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md index 1251fb2..bd0ccfc 100644 --- a/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md +++ b/Docs/Cmdlets/Set-NTFSSecurityDescriptor.md @@ -95,7 +95,7 @@ Parameter Sets: (All) Aliases: Required: True -Position: 2 +Position: 1 Default value: None Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False diff --git a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs index 48bb106..3d1a456 100644 --- a/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs +++ b/NTFSSecurity/SecurityDescriptorCmdlets/SetSecurityDescriptor.cs @@ -11,7 +11,7 @@ namespace NTFSSecurity { private SwitchParameter passThru; - [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public FileSystemSecurity2[] SecurityDescriptor { get { return securityDescriptors.ToArray(); } diff --git a/Tests/SecurityDescriptor.Tests.ps1 b/Tests/SecurityDescriptor.Tests.ps1 index 32ad555..e2c1156 100644 --- a/Tests/SecurityDescriptor.Tests.ps1 +++ b/Tests/SecurityDescriptor.Tests.ps1 @@ -73,6 +73,24 @@ Describe 'Get-NTFSSecurityDescriptor' { } Describe 'Set-NTFSSecurityDescriptor' { + Context 'Parameter declaration' { + # Its siblings (Get-NTFSSecurityDescriptor and the cmdlets with a -SecurityDescriptor parameter set) take + # their first positional parameter at 1; -SecurityDescriptor was the only one at 2 (6c), cosmetic only: a + # positional call with one argument already bound to it, because it was the only positional parameter. + It 'Should declare -SecurityDescriptor at position 1, and still accept it positionally' { + (Get-Command -Name 'Set-NTFSSecurityDescriptor').Parameters['SecurityDescriptor'].Attributes.Position | Should -Be 1 + + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'PositionalWrite' + Assert-TestSandboxPath -Sandbox $sandbox -Path $file + $sd = Get-NTFSSecurityDescriptor -Path $file + Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData + + Set-NTFSSecurityDescriptor $sd + + @(Get-EveryoneRule -Path $file) | Should -HaveCount 1 + } + } + Context 'When a cmdlet added an access entry to the descriptor' { It 'Should write the entry as the only explicit entry of the item' { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddedEntry' From 46a0cb0c7ade0232b04bff35a1d718b1001b62ba Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:23:15 +0000 Subject: [PATCH 15/36] docs(access): say that a local standard user still gets "Access is denied" for a domain user account Get-NTFSEffectiveAccess already falls back to the local authorization manager for a name of this computer when the remote one refuses the caller, which covers every account except a user account of the domain: Windows still answers "Access is denied" for that one case, because the local manager has no domain credentials to resolve it. The cmdlet already writes this as a non-terminating GetEffectiveAccessError and returns no result for the item; nothing in the behavior changes. Docs\Cmdlets\Get-NTFSEffectiveAccess.md gets one more NOTES paragraph, in the style of the neighboring "Before 5.0.0..." paragraphs, saying so and naming the accounts that still get a result (accounts of this computer, and domain groups such as Everyone or Domain Users). Evidence: Tests\Lab\Acceptance-2026-10-10-os-matrix.md, "The probe", row "Local standard user". No code change; Tests\Help.Tests.ps1 (unchanged) already covers the page's synopsis, parameters, examples, and links. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Get-NTFSEffectiveAccess.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md index d64e3ca..167c622 100644 --- a/Docs/Cmdlets/Get-NTFSEffectiveAccess.md +++ b/Docs/Cmdlets/Get-NTFSEffectiveAccess.md @@ -190,6 +190,8 @@ Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is de Before 5.0.0-rc8, the cmdlet returned no access and no error when Windows refused to start the authorization manager for the computer of `-ServerName`, for example for a name of 32,718 characters or more, so that the result looked like an account without any rights. The cmdlet now writes a non-terminating `GetEffectiveAccessError` with the error that Windows reported and returns no result for the item, as it does for every other failure of the calculation. It warns that the result is based on this computer only when it calculated a result; before 5.0.0-rc8, the warning also came before an error when the computer couldn't be reached. +A user who has no domain credentials, such as a local standard user, still gets the error "Access is denied" from Windows when it asks for the effective access of a user account of the domain, also for a name of this computer; the cmdlet writes it as a non-terminating `GetEffectiveAccessError` and returns no result for that item. Accounts of this computer and groups such as `Everyone` or `Domain Users` give a result. + ## RELATED LINKS [Get-NTFSAccess](Get-NTFSAccess.md) From 21629d3415cea6e6bd756674dc22d52f2b82dedf Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 13:14:58 +0000 Subject: [PATCH 16/36] test(items,owner,access): bind four parameters by property name The coverage record of Handoff 5 listed getters of parameters that take pipeline input by property name and that no test bound that way: -Destination of Copy-Item2 and Move-Item2, -Account of Set-NTFSOwner, and -Account of Get-NTFSEffectiveAccess (the getters of AccessType, AuditFlags, and the scope and flag parameters of the access and audit cmdlets, and -Algorithm of Get-FileHash2, are bound by tests now). PowerShell calls the getter of such a parameter once per command when a piped object binds it, so a test has to pipe an object that has the property. Each test pipes two objects that name different values and checks that each item gets its own: two destinations for the copied and the moved file (Copy-Item2 and Move-Item2, read back with Get-Content and Test-Path), two owners that only the Restore privilege can assign (Set-NTFSOwner, read back with Get-Acl; elevated only), and the effective access of Everyone, which an allow entry grants, and of Guests, which a deny entry takes away (Get-NTFSEffectiveAccess). Tests only; no product change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Tests/Access.Tests.ps1 | 20 ++++++++++++++ Tests/Owner.Tests.ps1 | 18 +++++++++++++ Tests/ParameterSets.Items.Tests.ps1 | 42 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index 5b70d0f..fbdbb33 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -88,6 +88,26 @@ Describe 'Get-NTFSEffectiveAccess' { $result | Should -BeNullOrEmpty } + # Each piped object names its own account, so one call calculates the access of two accounts on the same item. + It 'Should bind -Account by property name and calculate the access of the account that its own object names' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveAccountByProperty' + Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop + Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-NTFSAccess -Path $file -Account 'S-1-5-32-546' -AccessRights ReadData -AccessType Deny -ErrorAction Stop + $requests = @( + [pscustomobject]@{ Path = $file; Account = 'S-1-1-0' } + [pscustomobject]@{ Path = $file; Account = 'S-1-5-32-546' } + ) + + $result = @($requests | Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop) + + $result | Should -HaveCount 2 + $result[0].Account.Sid | Should -Be 'S-1-1-0' + $result[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue + $result[1].Account.Sid | Should -Be 'S-1-5-32-546' + $result[1].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeFalse + } + It 'Should return an account with access when -ExcludeNoneAccessEntries is used' { $result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop) diff --git a/Tests/Owner.Tests.ps1 b/Tests/Owner.Tests.ps1 index 324f749..d923a59 100644 --- a/Tests/Owner.Tests.ps1 +++ b/Tests/Owner.Tests.ps1 @@ -237,6 +237,24 @@ Describe 'Set-NTFSOwner' { ($result.FullName -join '|') | Should -Be ($files -join '|') } + # Each piped object names its own owner; the owners are accounts that only the Restore privilege can assign, so + # that the result differs from the owner that the items have already. + It 'Should bind -Account by property name and give each piped item the owner that its own object names' -Skip:(-not $canAssignAnyOwner) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerAccountByPropertyFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerAccountByPropertySecond' + $administrators = 'S-1-5-32-544' + $requests = @( + [pscustomobject]@{ Path = $first; Account = $trustedInstaller } + [pscustomobject]@{ Path = $second; Account = $administrators } + ) + + $result = @($requests | Set-NTFSOwner -PassThru -ErrorAction Stop) + + $result | Should -HaveCount 2 + Get-TestOwner -Path $first | Should -Be $trustedInstaller + Get-TestOwner -Path $second | Should -Be $administrators + } + It 'Should set an owner that only the Restore privilege allows' -Skip:(-not $canAssignAnyOwner) { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerRestore' diff --git a/Tests/ParameterSets.Items.Tests.ps1 b/Tests/ParameterSets.Items.Tests.ps1 index 7805650..1e4ae31 100644 --- a/Tests/ParameterSets.Items.Tests.ps1 +++ b/Tests/ParameterSets.Items.Tests.ps1 @@ -1206,6 +1206,48 @@ Describe 'Get-FileHash2 binds -Algorithm by property name' { } } +Describe 'Copy-Item2 and Move-Item2 bind -Destination by property name' { + # A piped object binds -Destination by its own property, so two objects can go to two destinations in one call. + It ' should put each piped item in the destination that its own object names' -ForEach @( + @{ Command = 'Copy-Item2' } + @{ Command = 'Move-Item2' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name "DestinationByProperty$Command" -Directory + $destinationOne = Join-Path -Path $folder -ChildPath 'One' + $destinationTwo = Join-Path -Path $folder -ChildPath 'Two' + $first = Join-Path -Path $folder -ChildPath 'First.txt' + $second = Join-Path -Path $folder -ChildPath 'Second.txt' + Assert-TestSandboxPath -Sandbox $sandbox -Path $destinationOne, $destinationTwo, $first, $second + New-Item -ItemType Directory -Path $destinationOne, $destinationTwo | Out-Null + Set-Content -LiteralPath $first -Value 'First' + Set-Content -LiteralPath $second -Value 'Second' + $requests = @( + [pscustomobject]@{ Path = $first; Destination = $destinationOne } + [pscustomobject]@{ Path = $second; Destination = $destinationTwo } + ) + + $result = @($requests | & $Command -PassThru $true -ErrorAction Stop) + + $copiedFirst = Join-Path -Path $destinationOne -ChildPath 'First.txt' + $copiedSecond = Join-Path -Path $destinationTwo -ChildPath 'Second.txt' + $result | Should -HaveCount 2 + $result[0].FullName | Should -Be $copiedFirst + $result[1].FullName | Should -Be $copiedSecond + Get-Content -LiteralPath $copiedFirst | Should -Be 'First' + Get-Content -LiteralPath $copiedSecond | Should -Be 'Second' + (Join-Path -Path $destinationOne -ChildPath 'Second.txt') | Should -Not -Exist + (Join-Path -Path $destinationTwo -ChildPath 'First.txt') | Should -Not -Exist + if ($Command -eq 'Copy-Item2') { + $first | Should -Exist + $second | Should -Exist + } + else { + $first | Should -Not -Exist + $second | Should -Not -Exist + } + } +} + Describe 'Get-DiskSpace free-space values and validation error' { It 'Should report free-space values close to an independent .NET read for the system drive' { $before = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive From abdf212fe4d55f9454e146cc268caa9d7a3d9e97 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:28:45 +0000 Subject: [PATCH 17/36] fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit silently kept the default value of AccessType, AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's property could not be converted to the parameter's type: a misspelled AccessType such as "Denyy", or a blank CSV cell, created an Allow entry with no message. For a permissions tool, a typo for Deny that silently grants access is a security-relevant defect (Decision 27). The five parameters are now declared as object and converted inside the cmdlet, the way the PowerShell binder converts a typed parameter (names in any case, comma-separated flag lists, numbers, enum instances). A value that cannot be converted, including a blank one, is now an error named InvalidParameterValue: terminating and catchable when it is named on the command line, exactly as the binder reported it before; a non-terminating error targeting that path or descriptor when it comes from a piped object, which leaves that item unchanged and lets the pipeline continue with the next one. A $null value is still not an error and keeps the parameter's default. Tab completion of the five parameters keeps working through explicit argument completers. Choosing between the Simple and Complex parameter sets is unaffected and still rejects AppliesTo together with InheritanceFlags or PropagationFlags as AmbiguousParameterSet, for every mix of string and enum values. The four cmdlet pages document the parameters as Type: Object, list the valid values, and describe the new error; a sentence that said a piped AppliesTo typo or blank cell was silently ignored is corrected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Add-NTFSAccess.md | 30 +- Docs/Cmdlets/Add-NTFSAudit.md | 28 +- Docs/Cmdlets/Remove-NTFSAccess.md | 30 +- Docs/Cmdlets/Remove-NTFSAudit.md | 28 +- NTFSSecurity/AccessCmdlets/AddAccess.cs | 128 +++++++- NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 128 +++++++- NTFSSecurity/AuditCmdlets/AddAudit.cs | 128 +++++++- NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 128 +++++++- NTFSSecurity/EnumArgumentCompleters.cs | 87 ++++++ NTFSSecurity/NTFSSecurity.csproj | 2 + NTFSSecurity/ParameterConversion.cs | 67 ++++ Tests/ParameterSets.Access.Tests.ps1 | 337 +++++++++++++++++++- Tests/ParameterSets.Audit.Tests.ps1 | 344 ++++++++++++++++++++- 13 files changed, 1341 insertions(+), 124 deletions(-) create mode 100644 NTFSSecurity/EnumArgumentCompleters.cs create mode 100644 NTFSSecurity/ParameterConversion.cs diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 3938707..3826c4e 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -16,28 +16,28 @@ Adds an access control entry (ACE) to a file, a folder, or a security descriptor ### PathSimple ``` Add-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] -AppliesTo [-PassThru] [] + [-AccessType ] -AppliesTo [-PassThru] [] ``` ### PathComplex ``` Add-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-PassThru] [] ``` ### SDSimple ``` Add-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] -AppliesTo [-PassThru] + [-AccessRights] [-AccessType ] -AppliesTo [-PassThru] [] ``` ### SDComplex ``` Add-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessRights] [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DAC `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see [Concepts](../Concepts.md). -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. @@ -107,10 +107,10 @@ Accept wildcard characters: False ### -AccessType -Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. +Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AccessControlType +Type: Object Parameter Sets: (All) Aliases: AccessControlType Accepted values: Allow, Deny @@ -140,10 +140,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. +Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -157,12 +157,12 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. +Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the [InheritanceFlags Enum](https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -208,10 +208,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. +Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index 3ee64ab..ec2410b 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -16,28 +16,28 @@ Adds an audit entry to a file or folder. ### PathSimple ``` Add-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] -AppliesTo [-PassThru] [] + [-AuditFlags ] -AppliesTo [-PassThru] [] ``` ### PathComplex ``` Add-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-PassThru] [] ``` ### SDSimple ``` Add-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] -AppliesTo [-PassThru] + [-AccessRights] [-AuditFlags ] -AppliesTo [-PassThru] [] ``` ### SDComplex ``` Add-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] + [-PropagationFlags ] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. -A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. @@ -125,10 +125,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. +Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -142,10 +142,10 @@ Accept wildcard characters: False ### -AuditFlags -Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. +Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AuditFlags +Type: Object Parameter Sets: (All) Aliases: Accepted values: None, Success, Failure @@ -159,10 +159,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. +Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -208,10 +208,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. +Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index b849ae3..195e0b9 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -16,28 +16,28 @@ Removes rights from the access control entries (ACEs) of a file, a folder, or a ### PathSimple ``` Remove-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] [] + [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### PathComplex ``` Remove-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ### SDSimple ``` Remove-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] + [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### SDComplex ``` Remove-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessRights] [-AccessType ] [-InheritanceFlags ] + [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ## DESCRIPTION @@ -48,7 +48,7 @@ Only the specified rights are taken away: when an entry grants more than `-Acces Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. ## EXAMPLES @@ -105,10 +105,10 @@ Accept wildcard characters: False ### -AccessType -Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. +Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AccessControlType +Type: Object Parameter Sets: (All) Aliases: AccessControlType Accepted values: Allow, Deny @@ -138,10 +138,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. +Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -155,10 +155,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. +Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -204,10 +204,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. +Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index fe4d1af..78e4516 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -16,28 +16,28 @@ Removes an audit entry from a file or folder. ### PathSimple ``` Remove-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] [] + [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### PathComplex ``` Remove-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ### SDSimple ``` Remove-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] + [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] [] ``` ### SDComplex ``` Remove-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessRights] [-AuditFlags ] [-InheritanceFlags ] + [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] ``` ## DESCRIPTION @@ -46,7 +46,7 @@ The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access cont Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. -In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -123,10 +123,10 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. +Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ```yaml -Type: ApplyTo +Type: Object Parameter Sets: PathSimple, SDSimple Aliases: Accepted values: ThisFolderOnly, ThisFolderSubfoldersAndFiles, ThisFolderAndSubfolders, ThisFolderAndFiles, SubfoldersAndFilesOnly, SubfoldersOnly, FilesOnly, ThisFolderSubfoldersAndFilesOneLevel, ThisFolderAndSubfoldersOneLevel, ThisFolderAndFilesOneLevel, SubfoldersAndFilesOnlyOneLevel, SubfoldersOnlyOneLevel, FilesOnlyOneLevel @@ -140,10 +140,10 @@ Accept wildcard characters: False ### -AuditFlags -Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. +Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: AuditFlags +Type: Object Parameter Sets: (All) Aliases: Accepted values: None, Success, Failure @@ -157,10 +157,10 @@ Accept wildcard characters: False ### -InheritanceFlags -Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. +Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: InheritanceFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, ContainerInherit, ObjectInherit @@ -206,10 +206,10 @@ Accept wildcard characters: False ### -PropagationFlags -Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. +Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. ```yaml -Type: PropagationFlags +Type: Object Parameter Sets: PathComplex, SDComplex Aliases: Accepted values: None, NoPropagateInherit, InheritOnly diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 5907a1a..03c907b 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AccessControlType accessType = AccessControlType.Allow; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object accessType; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool passThru; [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] @@ -65,7 +65,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("AccessControlType")] - public AccessControlType AccessType + [ArgumentCompleter(typeof(AccessControlTypeArgumentCompleter))] + public object AccessType { get { return accessType; } set { accessType = value; } @@ -73,7 +74,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -81,7 +83,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -90,7 +93,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -105,19 +109,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(accessType, "AccessType"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AccessControlType accessTypeValue; + if (!TryConvertValue(accessType, "AccessType", AccessControlType.Allow, out accessTypeValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -138,7 +177,7 @@ namespace NTFSSecurity try { - FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation); + FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); } catch (UnauthorizedAccessException) { @@ -146,7 +185,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation); + FileSystemAccessRule2.AddFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); }); } catch (Exception ex2) @@ -171,7 +210,7 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessType, inheritance, propagation); + FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); if (passThru == true) { @@ -185,5 +224,66 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index 757ac61..c0884b1 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AccessControlType accessType = AccessControlType.Allow; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object accessType; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool removeSpecific; private bool passThru; @@ -66,7 +66,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("AccessControlType")] - public AccessControlType AccessType + [ArgumentCompleter(typeof(AccessControlTypeArgumentCompleter))] + public object AccessType { get { return accessType; } set { accessType = value; } @@ -74,7 +75,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -82,7 +84,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -91,7 +94,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -116,19 +120,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(accessType, "AccessType"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AccessControlType accessTypeValue; + if (!TryConvertValue(accessType, "AccessType", AccessControlType.Allow, out accessTypeValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -149,7 +188,7 @@ namespace NTFSSecurity try { - FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); } catch (UnauthorizedAccessException) { @@ -157,7 +196,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + FileSystemAccessRule2.RemoveFileSystemAccessRule(item, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); }); } catch (Exception ex2) @@ -182,7 +221,7 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessType, inheritance, propagation, removeSpecific); + FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); if (passThru == true) { @@ -196,5 +235,66 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index e9ea655..43ca42f 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AuditFlags auditFlags = AuditFlags.Failure | AuditFlags.Success; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object auditFlags; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool passThru; [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] @@ -64,7 +64,8 @@ namespace NTFSSecurity } [Parameter(ValueFromPipelineByPropertyName = true)] - public AuditFlags AuditFlags + [ArgumentCompleter(typeof(AuditFlagsArgumentCompleter))] + public object AuditFlags { get { return auditFlags; } set { auditFlags = value; } @@ -72,7 +73,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -80,7 +82,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -89,7 +92,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -104,19 +108,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(auditFlags, "AuditFlags"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AuditFlags auditFlagsValue; + if (!TryConvertValue(auditFlags, "AuditFlags", System.Security.AccessControl.AuditFlags.Failure | System.Security.AccessControl.AuditFlags.Success, out auditFlagsValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -137,7 +176,7 @@ namespace NTFSSecurity try { - FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation); + FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); } catch (UnauthorizedAccessException) { @@ -145,7 +184,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation); + FileSystemAuditRule2.AddFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); }); } catch (Exception ex2) @@ -175,7 +214,7 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlags, inheritance, propagation); + FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); if (passThru == true) { @@ -189,5 +228,66 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index 352b65d..2814010 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -13,10 +13,10 @@ namespace NTFSSecurity { private IdentityReference2[] account; private FileSystemRights2 accessRights; - private AuditFlags auditFlags = AuditFlags.Failure | AuditFlags.Success; - private InheritanceFlags inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; - private PropagationFlags propagationFlags = PropagationFlags.None; - private ApplyTo appliesTo = ApplyTo.ThisFolderSubfoldersAndFiles; + private object auditFlags; + private object inheritanceFlags; + private object propagationFlags; + private object appliesTo; private bool removeSpecific; private bool passThru; @@ -65,7 +65,8 @@ namespace NTFSSecurity } [Parameter(ValueFromPipelineByPropertyName = true)] - public AuditFlags AuditFlags + [ArgumentCompleter(typeof(AuditFlagsArgumentCompleter))] + public object AuditFlags { get { return auditFlags; } set { auditFlags = value; } @@ -73,7 +74,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public InheritanceFlags InheritanceFlags + [ArgumentCompleter(typeof(InheritanceFlagsArgumentCompleter))] + public object InheritanceFlags { get { return inheritanceFlags; } set { inheritanceFlags = value; } @@ -81,7 +83,8 @@ namespace NTFSSecurity [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "SDComplex")] - public PropagationFlags PropagationFlags + [ArgumentCompleter(typeof(PropagationFlagsArgumentCompleter))] + public object PropagationFlags { get { return propagationFlags; } set { propagationFlags = value; } @@ -90,7 +93,8 @@ namespace NTFSSecurity // Mandatory, so that a command without it resolves to the Complex parameter set, also for -SecurityDescriptor. [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathSimple")] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "SDSimple")] - public ApplyTo AppliesTo + [ArgumentCompleter(typeof(ApplyToArgumentCompleter))] + public object AppliesTo { get { return appliesTo; } set { appliesTo = value; } @@ -115,19 +119,54 @@ namespace NTFSSecurity protected override void BeginProcessing() { + // Checked here, before privileges are enabled, because only a value that came from the command line is + // already bound at this point; a bad value from the pipeline is handled per record in ProcessRecord. + ValidateNamedValue(auditFlags, "AuditFlags"); + ValidateNamedValue(inheritanceFlags, "InheritanceFlags"); + ValidateNamedValue(propagationFlags, "PropagationFlags"); + ValidateNamedValue(appliesTo, "AppliesTo"); + base.BeginProcessing(); } protected override void ProcessRecord() { + AuditFlags auditFlagsValue; + if (!TryConvertValue(auditFlags, "AuditFlags", System.Security.AccessControl.AuditFlags.Failure | System.Security.AccessControl.AuditFlags.Success, out auditFlagsValue)) + { + return; + } + // The flags of this record are locals: the fields behind -InheritanceFlags and -PropagationFlags keep the value of the // previous piped object when the next one binds neither, so the scope that -AppliesTo converts to must not stay in them. - var inheritance = inheritanceFlags; - var propagation = propagationFlags; + var inheritance = System.Security.AccessControl.InheritanceFlags.ContainerInherit | System.Security.AccessControl.InheritanceFlags.ObjectInherit; + var propagation = System.Security.AccessControl.PropagationFlags.None; if (ParameterSetName.EndsWith("Simple")) { - FileSystemSecurity2.ConvertToFileSystemFlags(appliesTo, out inheritance, out propagation); + ApplyTo appliesToValue; + if (!TryConvertValue(appliesTo, "AppliesTo", default(ApplyTo), out appliesToValue)) + { + return; + } + + FileSystemSecurity2.ConvertToFileSystemFlags(appliesToValue, out inheritance, out propagation); + } + else + { + InheritanceFlags inheritanceValue; + if (!TryConvertValue(inheritanceFlags, "InheritanceFlags", inheritance, out inheritanceValue)) + { + return; + } + inheritance = inheritanceValue; + + PropagationFlags propagationValue; + if (!TryConvertValue(propagationFlags, "PropagationFlags", propagation, out propagationValue)) + { + return; + } + propagation = propagationValue; } if (ParameterSetName.StartsWith("Path")) @@ -148,7 +187,7 @@ namespace NTFSSecurity try { - FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); } catch (UnauthorizedAccessException) { @@ -156,7 +195,7 @@ namespace NTFSSecurity { InvokeAsOwner(item, path, () => { - FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + FileSystemAuditRule2.RemoveFileSystemAuditRule(item, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); }); } catch (Exception ex2) @@ -186,7 +225,7 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlags, inheritance, propagation, removeSpecific); + FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); if (passThru == true) { @@ -200,5 +239,66 @@ namespace NTFSSecurity { base.EndProcessing(); } + + // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there + // as a terminating error, the way the binder itself would have reported it for a typed parameter. + private void ValidateNamedValue(object value, string parameterName) + { + if (value == null) + { + return; + } + + try + { + ParameterConversion.Convert(value, parameterName); + } + catch (ArgumentException ex) + { + ThrowTerminatingError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); + } + } + + // A bad value that a piped object bound converts here, once per record. A null rawValue means the property + // was not bound for this record, so the parameter's own default applies. A bad value is reported as one + // non-terminating error per path or per descriptor, and the caller leaves the record alone and returns. + private bool TryConvertValue(object rawValue, string parameterName, T defaultValue, out T result) + { + if (rawValue == null) + { + result = defaultValue; + return true; + } + + try + { + result = ParameterConversion.Convert(rawValue, parameterName); + return true; + } + catch (ArgumentException ex) + { + result = defaultValue; + WriteInvalidParameterValueErrors(ex); + return false; + } + } + + private void WriteInvalidParameterValueErrors(Exception ex) + { + if (ParameterSetName.StartsWith("Path")) + { + foreach (var path in paths) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, path)); + } + } + else + { + foreach (var sd in securityDescriptors) + { + WriteError(new ErrorRecord(ex, "InvalidParameterValue", ErrorCategory.InvalidArgument, sd)); + } + } + } } } \ No newline at end of file diff --git a/NTFSSecurity/EnumArgumentCompleters.cs b/NTFSSecurity/EnumArgumentCompleters.cs new file mode 100644 index 0000000..87604d6 --- /dev/null +++ b/NTFSSecurity/EnumArgumentCompleters.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Security.AccessControl; +using Security2; + +namespace NTFSSecurity +{ + // Completes the names of an enumeration for a parameter that is declared as object (Decision 27, fix B) and so + // does not get the completion that a parameter of an enumeration type gets from PowerShell itself. + internal static class EnumArgumentCompletion + { + /// + /// Returns the names of the enumeration that start with the word the user already typed, compared without + /// regard to case, in the order Enum.GetNames returns them. + /// + /// The enumeration to complete the names of. + /// The word the user already typed; empty or null completes every name. + internal static IEnumerable Complete(Type enumType, string wordToComplete) + { + var word = wordToComplete ?? string.Empty; + var results = new List(); + + foreach (var name in Enum.GetNames(enumType)) + { + if (name.StartsWith(word, StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(name, name, CompletionResultType.ParameterValue, name)); + } + } + + return results; + } + } + + /// Completes the names of System.Security.AccessControl.AccessControlType for -AccessType. + public class AccessControlTypeArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(AccessControlType), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.AuditFlags for -AuditFlags. + public class AuditFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(AuditFlags), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.InheritanceFlags for -InheritanceFlags. + public class InheritanceFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(InheritanceFlags), wordToComplete); + } + } + + /// Completes the names of System.Security.AccessControl.PropagationFlags for -PropagationFlags. + public class PropagationFlagsArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(PropagationFlags), wordToComplete); + } + } + + /// Completes the names of Security2.ApplyTo for -AppliesTo. + public class ApplyToArgumentCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + return EnumArgumentCompletion.Complete(typeof(ApplyTo), wordToComplete); + } + } +} diff --git a/NTFSSecurity/NTFSSecurity.csproj b/NTFSSecurity/NTFSSecurity.csproj index bed8c19..d962e2f 100644 --- a/NTFSSecurity/NTFSSecurity.csproj +++ b/NTFSSecurity/NTFSSecurity.csproj @@ -73,6 +73,8 @@ + + diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs new file mode 100644 index 0000000..bb6241f --- /dev/null +++ b/NTFSSecurity/ParameterConversion.cs @@ -0,0 +1,67 @@ +using System; +using System.Management.Automation; + +namespace NTFSSecurity +{ + /// + /// Converts the raw value of a parameter that is declared as object back into the enumeration the parameter + /// describes, the way the PowerShell binder converts an argument for a typed parameter. AccessType, AuditFlags, + /// InheritanceFlags, PropagationFlags, and AppliesTo of the access and audit cmdlets are declared as object + /// (Decision 27, fix B), so that the binder does not silently skip a piped property it cannot convert and keep + /// the parameter at its default; this class gives the cmdlet the same conversion, with a message it can report + /// as its own error instead of a binding error. This class writes nothing; the cmdlet writes the error. + /// + internal static class ParameterConversion + { + /// + /// Converts the raw value of a parameter to T, the way the PowerShell binder converts an argument for a + /// parameter of that type: names in any case, a comma-separated list for a flags enumeration, a number, or + /// an enumeration instance. + /// + /// + /// The raw value of the parameter. Not null: the caller keeps the default of the parameter for null instead + /// of calling this method, because null means that the parameter was not specified. + /// + /// The name of the parameter, without the leading dash, for the message. + /// The converted value. + /// + /// The value is blank (an empty or a white-space-only string, the usual blank CSV cell) or the conversion + /// failed. The message names the value, unless it is blank, the parameter, and the valid names of T; the + /// inner exception of a failed conversion is the error that LanguagePrimitives.ConvertTo raised. + /// + internal static T Convert(object value, string parameterName) + { + var text = value as string; + if (text != null && string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException(string.Format( + "The value for parameter -{0} is blank. {1}", parameterName, DescribeValidValues(typeof(T)))); + } + + try + { + return (T)LanguagePrimitives.ConvertTo(value, typeof(T)); + } + catch (Exception ex) + { + throw new ArgumentException( + string.Format("The value '{0}' for parameter -{1} is not valid. {2}", value, parameterName, DescribeValidValues(typeof(T))), + ex); + } + } + + // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a + // combination of them is written separated with commas. + private static string DescribeValidValues(Type enumType) + { + var names = string.Join(", ", Enum.GetNames(enumType)); + + if (enumType.IsDefined(typeof(FlagsAttribute), false)) + { + return string.Format("Valid values: {0}. Combine several with a comma.", names); + } + + return string.Format("Valid values: {0}.", names); + } + } +} diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index bea7643..8d3094c 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -201,9 +201,10 @@ Describe 'Add-NTFSAccess' { $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit/None|ContainerInherit, ObjectInherit/InheritOnly' } - # A blank cell that Import-Csv reads is an empty string: not $null, and not a scope name. The binder cannot convert it, so - # PathSimple does not bind, and the row gets the default scope, as an object without the property does. - It 'Should give the row of a CSV file whose AppliesTo cell is blank the default scope, and the other rows their own' { + # A blank cell that Import-Csv reads is an empty string: not $null, and not a scope name. Since -AppliesTo is + # now an object parameter (Decision 27, fix B), the blank string still binds it, which selects PathSimple, + # so the row gets an InvalidParameterValue error instead of the default scope; the other rows are unaffected. + It 'Should write an InvalidParameterValue error for the row of a CSV file whose AppliesTo cell is blank, leave its folder alone, and still give the other rows their own scope' { $folders = @('AddCsvBlankFirst', 'AddCsvBlankCell', 'AddCsvBlankLast' | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) $csv = Join-Path -Path $sandbox -ChildPath 'AddCsvBlank.csv' @@ -215,10 +216,15 @@ Describe 'Add-NTFSAccess' { ('"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders[2]) ) - Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorAction Stop + Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorVariable addErrors -ErrorAction SilentlyContinue + $addErrors | Should -HaveCount 1 + $addErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $addErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $addErrors[0].TargetObject | Should -Be $folders[1] + $addErrors[0].Exception.Message | Should -Match 'AppliesTo' $scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl (Get-Acl -LiteralPath $_) -Account 'S-1-5-32-546' } - $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly' + $scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly' } # $null cannot be bound at all (ConvertFrom-Csv makes it of a trailing blank cell): the object fails, loudly, and the cmdlet goes @@ -1223,6 +1229,327 @@ Describe 'Get-NTFSEffectiveAccess' { } } +# Decision 27 (fix B): AccessType, InheritanceFlags, PropagationFlags, and AppliesTo of Add-NTFSAccess and +# Remove-NTFSAccess are object parameters, converted in the cmdlet with LanguagePrimitives.ConvertTo, so that a +# piped value the binder cannot convert to the enumeration is an error instead of a silently kept default. +Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationFlags, and AppliesTo' { + BeforeDiscovery { + $accessBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + ) + $accessTypeBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Denyy'; ValueText = 'Denyy' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + ) + + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5) = 40 rows. + $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + foreach ($shapeCase in $accessTypeBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + foreach ($parameterName in 'InheritanceFlags', 'PropagationFlags', 'AppliesTo') { + foreach ($shapeCase in $accessBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + } + + $accessSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Denyy'; ValueText = 'Denyy' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo'; ValueText = 'Typoo' } + } + + $accessNoCarryOverCases = @( + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Value = 'Denyy' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AppliesTo'; Value = 'Typoo' } + ) + + $accessNamedInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Denyy' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + } + + $accessValidAccessTypeShapeCases = @( + @{ Description = 'the exact name'; Value = 'Deny' } + @{ Description = 'a different case'; Value = 'deny' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AccessControlType]::Deny } + @{ Description = 'an integer'; Value = 1 } + ) + + $accessValidFlagsShapeCases = @( + @{ Description = 'a comma list without spaces'; Value = 'ContainerInherit,ObjectInherit' } + @{ Description = 'a comma list with spaces'; Value = 'ContainerInherit, ObjectInherit' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' } + ) + + $accessAmbiguousValueCases = @( + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'an enum value'; FlagIsEnum = $true } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'an enum value'; FlagIsEnum = $true } + ) + + $accessCompleterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; EnumTypeName = 'System.Security.AccessControl.AccessControlType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; EnumTypeName = 'System.Security.AccessControl.InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } + } + } + + Context 'A bad value bound from the pipeline' { + It 'Should write one InvalidParameterValue error for - (), change nothing, and still process the next object' -ForEach $accessInvalidParameterCases { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValue' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValueNext' -Directory + $before = (Get-Acl -LiteralPath $path).Sddl + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ Path = $path; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ Path = $next; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $pipelineErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $pipelineErrors[0].TargetObject | Should -Be $path + $pipelineErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) + if ($ValueText) { + $pipelineErrors[0].Exception.Message | Should -Match ([regex]::Escape($ValueText)) + } + (Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + + It 'Should write one InvalidParameterValue error per descriptor for - bound to a piped descriptor, and leave the descriptors unchanged' -ForEach $accessSdInvalidParameterCases { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdSecond' -Directory + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $pipelineErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $pipelineErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } + + Context 'Continuation and no carry-over across piped objects' { + It 'Should apply both good objects when a bad AccessType value is between them, in the order of the rows' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFirst' -Directory + $middle = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueMiddle' -Directory + $last = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueLast' -Directory + + $rows = @( + [pscustomobject]@{ Path = $first; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + [pscustomobject]@{ Path = $middle; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Denyy' } + [pscustomobject]@{ Path = $last; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].TargetObject | Should -Be $middle + $firstRule = @((Get-Acl -LiteralPath $first).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $firstRule | Should -HaveCount 1 + $firstRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + $middleRule = @((Get-Acl -LiteralPath $middle).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $middleRule | Should -BeNullOrEmpty + $lastRule = @((Get-Acl -LiteralPath $last).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $lastRule | Should -HaveCount 1 + $lastRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + # The rows are applied in order: both surviving entries show up, $middle's does not, and nothing reports twice. + @($result | Where-Object -FilterScript { $_.FullName -eq $first -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 + @($result | Where-Object -FilterScript { $_.FullName -eq $last -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 + @($result | Where-Object -FilterScript { $_.FullName -eq $middle -and $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + + It 'Should give the row without AccessType the default Allow after a preceding Deny row, with no carry-over' { + $denyFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDeny' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDefault' -Directory + + @( + [pscustomobject]@{ Path = $denyFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + [pscustomobject]@{ Path = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAccess -ErrorAction Stop + + $denyRule = @((Get-Acl -LiteralPath $denyFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $denyRule | Should -HaveCount 1 + $denyRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + $defaultRule = @((Get-Acl -LiteralPath $defaultFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + } + + It 'Should give the row without its own default after a preceding row with an invalid value, with no carry-over' -ForEach $accessNoCarryOverCases { + $badFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverBad' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverAfterBad' -Directory + $extra = @{ } + $extra[$Parameter] = $Value + + @( + [pscustomobject] (@{ Path = $badFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ Path = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | & $Cmdlet -ErrorAction SilentlyContinue + + $defaultRule = @((Get-Acl -LiteralPath $defaultFolder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + $defaultRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $defaultRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'Valid values are unchanged for every shape' { + It 'Should add a Deny entry for AccessType given as ' -ForEach $accessValidAccessTypeShapeCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessType' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $Value } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + } + + It 'Should keep the default AccessType Allow for a $null value' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessTypeNull' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $null } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Allow) + } + + It 'Should scope the entry with InheritanceFlags given as ' -ForEach $accessValidFlagsShapeCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidInheritanceFlags' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $Value; PropagationFlags = 'None' } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + } + + It 'Should keep the default flags for $null InheritanceFlags and PropagationFlags' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidFlagsNull' -Directory + + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $null; PropagationFlags = $null } | Add-NTFSAccess -ErrorAction Stop + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $rule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'A bad value named on the command line' { + It 'Should throw a terminating InvalidParameterValue error for -, catchable with try/catch, and change nothing' -ForEach $accessNamedInvalidParameterCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedInvalid' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $Value + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should stop before the first piped path when -AccessType is named and bad' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadSecond' -Directory + $before = @((Get-Acl -LiteralPath $first).Sddl, (Get-Acl -LiteralPath $second).Sddl) + + $caughtId = $null + try { + $first, $second | Add-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -AccessType Denyy -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $first).Sddl | Should -BeExactly $before[0] + (Get-Acl -LiteralPath $second).Sddl | Should -BeExactly $before[1] + } + } + + Context 'Choosing the parameter set with a mix of string and enum values' { + It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + $object = [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = $appliesToValue; InheritanceFlags = $flagValue } + + { $object | Add-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' + + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should reject a named call whose -AppliesTo is combined with -PropagationFlags as with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousNamedMix' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + + { Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo $appliesToValue -PropagationFlags $flagValue -ErrorAction Stop } | + Should -Throw -ErrorId 'AmbiguousParameterSet*' + + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + + Context 'Tab completion and the parameter type' { + It ' - should complete exactly the names of its enumeration' -ForEach $accessCompleterCases { + $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter + $enumType = [type] $EnumTypeName + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + + @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) | Should -BeExactly ([Enum]::GetNames($enumType)) + } + + It ' should declare - as [object]' -ForEach $accessCompleterCases { + (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) + } + } +} + Describe 'Declared output types and parameter set boundaries' { It ' should declare as its output type' -ForEach $declaredOutputTypes { @((Get-Command -Name $Name).OutputType.Name) | Should -Be $Types diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index a22289a..a4c6a7a 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -276,9 +276,10 @@ Describe 'Add-NTFSAudit' { $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit/None|ContainerInherit, ObjectInherit/InheritOnly' } - # A blank cell that Import-Csv reads is an empty string, not $null: the row gets the default scope (ParameterSets.Access.Tests.ps1 - # has the same rows and says why). - It 'Should give the row of a CSV file whose AppliesTo cell is blank the default scope, and the other rows their own' -Skip:(-not $canReadAudit) { + # A blank cell that Import-Csv reads is an empty string, not $null. Since -AppliesTo is now an object + # parameter (Decision 27, fix B), the blank string still binds it, which selects PathSimple, so the row gets + # an InvalidParameterValue error instead of the default scope (ParameterSets.Access.Tests.ps1 has the same rows). + It 'Should write an InvalidParameterValue error for the row of a CSV file whose AppliesTo cell is blank, leave its folder alone, and still give the other rows their own scope' -Skip:(-not $canReadAudit) { $folders = @('AddCsvAuditBlankFirst', 'AddCsvAuditBlankCell', 'AddCsvAuditBlankLast' | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) $csv = Join-Path -Path $sandbox -ChildPath 'AddCsvAuditBlank.csv' @@ -290,10 +291,14 @@ Describe 'Add-NTFSAudit' { ('"{0}",{1},ReadData,SubfoldersAndFilesOnly' -f $folders[2], $probe) ) - Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorAction Stop + Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorVariable auditErrors -ErrorAction SilentlyContinue + $auditErrors | Should -HaveCount 1 + $auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $auditErrors[0].TargetObject | Should -Be $folders[1] $scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } - $scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly' + $scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly' } # $null cannot be bound at all: the object fails, loudly, and the cmdlet goes on with the next one. @@ -914,3 +919,332 @@ Describe 'Get-NTFSOrphanedAudit' { } } } + +# Decision 27 (fix B): AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo of Add-NTFSAudit and +# Remove-NTFSAudit are object parameters, converted in the cmdlet with LanguagePrimitives.ConvertTo, so that a piped +# value the binder cannot convert to the enumeration is an error instead of a silently kept default. A bad value is +# caught by the conversion before the cmdlet reads or writes the SACL, so none of these tests need the Security +# privilege, unlike most of the tests above. +Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo' { + BeforeDiscovery { + $auditBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + ) + $auditFlagsBadShapes = @( + @{ Shape = 'a misspelled name'; Value = 'Failurre'; ValueText = 'Failurre' } + @{ Shape = 'a blank string'; Value = ''; ValueText = $null } + @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } + @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } + @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + ) + + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5) = 40 rows. + $auditInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + foreach ($shapeCase in $auditFlagsBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + foreach ($parameterName in 'InheritanceFlags', 'PropagationFlags', 'AppliesTo') { + foreach ($shapeCase in $auditBadShapes) { + @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } + } + } + + $auditSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = 'Failurre'; ValueText = 'Failurre' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo'; ValueText = 'Typoo' } + } + + $auditNoCarryOverCases = @( + @{ Parameter = 'AuditFlags'; Value = 'Failurre' } + @{ Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Parameter = 'AppliesTo'; Value = 'Typoo' } + ) + + $auditNamedInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = 'Failurre' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + } + + $auditValidAuditFlagsShapeCases = @( + @{ Description = 'the exact name'; Value = 'Failure' } + @{ Description = 'a different case'; Value = 'failure' } + @{ Description = 'a comma list without spaces'; Value = 'Success,Failure' } + @{ Description = 'a comma list with spaces'; Value = 'Success, Failure' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AuditFlags]::Failure } + @{ Description = 'an integer'; Value = 2 } + ) + + $auditValidFlagsShapeCases = @( + @{ Description = 'a comma list without spaces'; Value = 'ContainerInherit,ObjectInherit' } + @{ Description = 'a comma list with spaces'; Value = 'ContainerInherit, ObjectInherit' } + @{ Description = 'the enum value'; Value = [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' } + ) + + $auditAmbiguousValueCases = @( + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'a string'; FlagIsEnum = $false } + @{ AppliesToShape = 'a string'; AppliesToIsEnum = $false; FlagShape = 'an enum value'; FlagIsEnum = $true } + @{ AppliesToShape = 'an enum value'; AppliesToIsEnum = $true; FlagShape = 'an enum value'; FlagIsEnum = $true } + ) + + $auditCompleterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; EnumTypeName = 'System.Security.AccessControl.AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; EnumTypeName = 'System.Security.AccessControl.InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } + } + } + + Context 'A bad value bound from the pipeline' { + # Writing an audit entry needs the Security privilege, so without it the "good" object right next to the bad + # one fails too, with its own (unrelated) privilege error; that part of the test runs only with the privilege, + # the same way most of Audit.Tests.ps1 does. The conversion failure that this test pins happens before the + # cmdlet ever touches the SACL, so the core assertions below run in every configuration. + It 'Should write one InvalidParameterValue error for - (), change nothing, and still process the next object' -ForEach $auditInvalidParameterCases { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValue' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValueNext' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $path } + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ FullName = $path; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ FullName = $next; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $invalidValueErrors = @($pipelineErrors | Where-Object -FilterScript { $_.TargetObject -eq $path }) + $invalidValueErrors | Should -HaveCount 1 + $invalidValueErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $invalidValueErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $invalidValueErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) + if ($ValueText) { + $invalidValueErrors[0].Exception.Message | Should -Match ([regex]::Escape($ValueText)) + } + if ($canReadAudit) { + Get-TestAuditSddl -Path $path | Should -BeExactly $before + $pipelineErrors | Should -HaveCount 1 + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + + # A descriptor that Get-NTFSSecurityDescriptor read without the Security privilege has no audit section, so + # without the privilege the "good" descriptor also gets its own (unrelated) ReadSecurityError; see above. + It 'Should write one InvalidParameterValue error per descriptor for - bound to a piped descriptor, and leave the descriptors unchanged' -ForEach $auditSdInvalidParameterCases { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdSecond' -Directory + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + $extra = @{ } + $extra[$Parameter] = $Value + + $rows = @( + [pscustomobject] (@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) + + $result = @($rows | & $Cmdlet -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $invalidValueErrors = @($pipelineErrors | Where-Object -FilterScript { $_.TargetObject -eq $descriptors[0] }) + $invalidValueErrors | Should -HaveCount 1 + $invalidValueErrors[0].FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*' + $invalidValueErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + if ($canReadAudit) { + $pipelineErrors | Should -HaveCount 1 + } + } + } + + Context 'Continuation and no carry-over across piped objects' { + It 'Should apply both good objects when a bad AuditFlags value is between them, in the order of the rows' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFirst' -Directory + $middle = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueMiddle' -Directory + $last = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueLast' -Directory + + $rows = @( + [pscustomobject]@{ FullName = $first; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + [pscustomobject]@{ FullName = $middle; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failurre' } + [pscustomobject]@{ FullName = $last; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + ) + + $rows | Add-NTFSAudit -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].TargetObject | Should -Be $middle + $firstRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $first -Audit) -Account 'S-1-1-0' + $firstRule | Should -HaveCount 1 + $firstRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + $middleRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $middle -Audit) -Account 'S-1-1-0' + $middleRule | Should -BeNullOrEmpty + $lastRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $last -Audit) -Account 'S-1-1-0' + $lastRule | Should -HaveCount 1 + $lastRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + } + + It 'Should give the row without AuditFlags the default Success and Failure after a preceding Failure-only row, with no carry-over' -Skip:(-not $canReadAudit) { + $failureFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverFailure' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDefault' -Directory + + @( + [pscustomobject]@{ FullName = $failureFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = 'Failure' } + [pscustomobject]@{ FullName = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAudit -ErrorAction Stop + + $failureRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $failureFolder -Audit) -Account 'S-1-1-0' + $failureRule | Should -HaveCount 1 + $failureRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Failure) + $defaultRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $defaultFolder -Audit) -Account 'S-1-1-0' + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + } + + It 'Should give the row without its own default after a preceding row with an invalid value, with no carry-over' -ForEach $auditNoCarryOverCases -Skip:(-not $canReadAudit) { + $badFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverBad' -Directory + $defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverAfterBad' -Directory + $extra = @{ } + $extra[$Parameter] = $Value + + @( + [pscustomobject] (@{ FullName = $badFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) + [pscustomobject]@{ FullName = $defaultFolder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + ) | Add-NTFSAudit -ErrorAction SilentlyContinue + + $defaultRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $defaultFolder -Audit) -Account 'S-1-1-0' + $defaultRule | Should -HaveCount 1 + $defaultRule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + $defaultRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $defaultRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'Valid values are unchanged for every shape' { + It 'Should add a Failure-only entry for AuditFlags given as ' -ForEach $auditValidAuditFlagsShapeCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAuditFlags' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = $Value } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].AuditFlags.HasFlag([System.Security.AccessControl.AuditFlags]::Failure) | Should -BeTrue + } + + It 'Should keep the default AuditFlags Success and Failure for a $null value' -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAuditFlagsNull' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AuditFlags = $null } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags] 'Success, Failure') + } + + It 'Should scope the entry with InheritanceFlags given as ' -ForEach $auditValidFlagsShapeCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidInheritanceFlags' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $Value; PropagationFlags = 'None' } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + } + + It 'Should keep the default flags for $null InheritanceFlags and PropagationFlags' -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidFlagsNull' -Directory + + [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = $null; PropagationFlags = $null } | Add-NTFSAudit -ErrorAction Stop + + $rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $folder -Audit) -Account 'S-1-1-0' + $rule | Should -HaveCount 1 + $rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') + $rule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) + } + } + + Context 'A bad value named on the command line' { + It 'Should throw a terminating InvalidParameterValue error for -, catchable with try/catch, and change nothing' -ForEach $auditNamedInvalidParameterCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedInvalid' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $Value + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should stop before the first piped path when -AuditFlags is named and bad' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadFirst' -Directory + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadSecond' -Directory + $before = if ($canReadAudit) { @((Get-TestAuditSddl -Path $first), (Get-TestAuditSddl -Path $second)) } + + $caughtId = $null + try { + $first, $second | Add-NTFSAudit -Account 'S-1-1-0' -AccessRights ReadData -AuditFlags Failurre -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $first | Should -BeExactly $before[0] + Get-TestAuditSddl -Path $second | Should -BeExactly $before[1] + } + } + } + + Context 'Choosing the parameter set with a mix of string and enum values' { + It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $auditAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + $object = [pscustomobject]@{ FullName = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = $appliesToValue; InheritanceFlags = $flagValue } + + { $object | Add-NTFSAudit -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' + } + + It 'Should reject a named call whose -AppliesTo is combined with -PropagationFlags as with an AmbiguousParameterSet error' -ForEach $auditAmbiguousValueCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousNamedMix' -Directory + $appliesToValue = if ($AppliesToIsEnum) { [Security2.ApplyTo]::ThisFolderOnly } else { 'ThisFolderOnly' } + $flagValue = if ($FlagIsEnum) { [System.Security.AccessControl.InheritanceFlags]::None } else { 'None' } + + { Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo $appliesToValue -PropagationFlags $flagValue -ErrorAction Stop } | + Should -Throw -ErrorId 'AmbiguousParameterSet*' + } + } + + Context 'Tab completion and the parameter type' { + It ' - should complete exactly the names of its enumeration' -ForEach $auditCompleterCases { + $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter + $enumType = [type] $EnumTypeName + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + + @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) | Should -BeExactly ([Enum]::GetNames($enumType)) + } + + It ' should declare - as [object]' -ForEach $auditCompleterCases { + (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) + } + } +} From 62699eeb7b98febc86d0a2ea1d8266f958e0a85c Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:44:08 +0000 Subject: [PATCH 18/36] fix(access): report a failing change of a descriptor as an error per descriptor In the four -SecurityDescriptor parameter sets of Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit, a change that throws, such as a Deny entry without rights, ended the cmdlet with a terminating error, while the -Path parameter sets already wrote an AddAceError or RemoveAceError for the failing item and went on to the next one. The two kinds of parameter sets now behave the same way. Each descriptor's change is wrapped in its own try/catch: a failure writes a non-terminating AddAceError or RemoveAceError, category WriteError, that targets the descriptor, and the cmdlet continues with the next descriptor in the pipeline instead of stopping. -PassThru stays outside the try, so what a later command raises when the cmdlet writes its output is never caught there. The existing TestAuditSection check of the audit cmdlets, and everything else about a successful change, is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/AccessCmdlets/AddAccess.cs | 10 +- NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 10 +- NTFSSecurity/AuditCmdlets/AddAudit.cs | 10 +- NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 10 +- Tests/ParameterSets.Access.Tests.ps1 | 104 +++++++++++++++++++++ Tests/ParameterSets.Audit.Tests.ps1 | 81 ++++++++++++++++ 6 files changed, 221 insertions(+), 4 deletions(-) diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 03c907b..249874d 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -210,7 +210,15 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); + try + { + FileSystemAccessRule2.AddFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index c0884b1..d2ac020 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -221,7 +221,15 @@ namespace NTFSSecurity { foreach (var sd in securityDescriptors) { - FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); + try + { + FileSystemAccessRule2.RemoveFileSystemAccessRule(sd, account.ToList(), accessRights, accessTypeValue, inheritance, propagation, removeSpecific); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index 43ca42f..fa3f8da 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -214,7 +214,15 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); + try + { + FileSystemAuditRule2.AddFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AddAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index 2814010..6d308e1 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -225,7 +225,15 @@ namespace NTFSSecurity continue; } - FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); + try + { + FileSystemAuditRule2.RemoveFileSystemAuditRule(sd, account.ToList(), accessRights, auditFlagsValue, inheritance, propagation, removeSpecific); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveAceError", ErrorCategory.WriteError, sd)); + continue; + } if (passThru == true) { diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 8d3094c..af20c36 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -418,6 +418,57 @@ Describe 'Add-NTFSAccess' { (Get-Acl -LiteralPath $second).Sddl | Should -BeExactly $before[1] } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET + # refuses to build, the same as Access.Tests.ps1:774 pins for -Path) reports an error for that descriptor and goes + # on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write an AddAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and $_.AccessControlType -eq 'Deny' }) | Should -HaveCount 1 + } + + It 'Should write an AddAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and $_.AccessControlType -eq 'Deny' }) | Should -HaveCount 1 + } + } } Describe 'Remove-NTFSAccess' { @@ -736,6 +787,59 @@ Describe 'Remove-NTFSAccess' { } } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET + # refuses to build, the same as Access.Tests.ps1:571 pins for -Path) reports an error for that descriptor and goes + # on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + Add-NTFSAccess -Path $first, $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -AppliesTo ThisFolderOnly -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + Add-NTFSAccess -Path $first, $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = 'S-1-1-0'; AccessRights = 'None'; AccessType = 'Deny' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = 'Deny' } + ) + + $result = @($rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + @($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + } } Describe 'Clear-NTFSAccess' { diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index a4c6a7a..046bfd4 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -357,6 +357,55 @@ Describe 'Add-NTFSAudit' { (Get-TestAccountAuditRule -Acl $sd.SecurityDescriptor -Account $probe) | Should -HaveCount 1 } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (an audit entry without rights, which + # .NET refuses to build, the same as Audit.Tests.ps1:141 pins for -Path) reports an error for that descriptor and + # goes on with the next one, instead of ending the cmdlet with a terminating error. + Context 'A failing change to one of several descriptors' { + It 'Should write an AddAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success'; AppliesTo = 'ThisFolderOnly' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success'; AppliesTo = 'ThisFolderOnly' } + ) + + $result = @($rows | Add-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + + It 'Should write an AddAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success' } + ) + + $result = @($rows | Add-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } } Describe 'Remove-NTFSAudit' { @@ -605,6 +654,38 @@ Describe 'Remove-NTFSAudit' { Get-TestDaclSddl -Path $file | Should -BeExactly $daclBefore } } + + # Item 5 of Decision 27: a failing change to one of several descriptors (an audit entry without rights, which + # .NET refuses to build, the same as Audit.Tests.ps1:141 pins for -Path) reports an error for that descriptor and + # goes on with the next one, instead of ending the cmdlet with a terminating error. Only the SDComplex set is + # listed as open in Quality-Gate-Test-Round-2026-10-20-ParameterSets.csv (row 26); SDSimple is already closed. + Context 'A failing change to one of several descriptors' { + It 'Should write a RemoveAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' -Skip:(-not $canReadAudit) { + $first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst' + $second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond' + # A keeper entry for a different account on both items: audit entries are not inherited by default, so + # without one, -PassThru of the descriptor that succeeds would show nothing once its only entry is gone. + Add-NTFSAudit -Path $first, $second -Account $probe -AccessRights ReadData -AuditFlags Success -ErrorAction Stop + Add-NTFSAudit -Path $first, $second -Account $keeper -AccessRights Delete -AuditFlags Success -ErrorAction Stop + $descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second -ErrorAction Stop) + $before = $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') + + $rows = @( + [pscustomobject]@{ SecurityDescriptor = $descriptors[0]; Account = $probe; AccessRights = 'None'; AuditFlags = 'Success' } + [pscustomobject]@{ SecurityDescriptor = $descriptors[1]; Account = $probe; AccessRights = 'ReadData'; AuditFlags = 'Success' } + ) + + $result = @($rows | Remove-NTFSAudit -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue) + + $sdErrors | Should -HaveCount 1 + $sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' + $sdErrors[0].CategoryInfo.Category | Should -Be 'WriteError' + $sdErrors[0].TargetObject | Should -Be $descriptors[0] + $descriptors[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $second } + } + } } Describe 'Clear-NTFSAudit' { From d3ba5a9a314c457abe6fb9692c2945a1a0d48c89 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:03:33 +0000 Subject: [PATCH 19/36] fix(access): stop for a named $null of the five parameters A named $null of AccessType, AuditFlags, InheritanceFlags, PropagationFlags, or AppliesTo (a literal $null, a variable that holds $null such as a misspelled property, the alias -AccessControlType, splatting, or a null entry of $PSDefaultParameterValues) silently kept the parameter's default instead of stopping: Add-NTFSAccess -AccessType $null created an Allow entry, Add-NTFSAudit -AuditFlags $null audited Success and Failure, and Remove-NTFSAccess -AccessType $null removed nothing. Before the five parameters became object (Decision 27, fix B), the same named $null ended with a terminating ParameterBindingFailed. Each cmdlet's BeginProcessing now checks, for a value that is null, whether MyInvocation.BoundParameters has the parameter's name; a piped property that is null is not bound yet at that point and still keeps the default, but a value that is already bound and null is reported the same way as a bad named value: a terminating InvalidParameterValue error naming the parameter and its valid values. AppliesTo is Mandatory and a named $null of it was already a terminating ParameterArgumentValidationErrorNullNotAllowed from the binder itself, unchanged by this fix. ParameterConversion gains the message for this case next to the existing blank-value message, so that the wording is built in one place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/AccessCmdlets/AddAccess.cs | 13 ++- NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 13 ++- NTFSSecurity/AuditCmdlets/AddAudit.cs | 13 ++- NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 13 ++- NTFSSecurity/ParameterConversion.cs | 13 +++ Tests/ParameterSets.Access.Tests.ps1 | 102 +++++++++++++++++++++ Tests/ParameterSets.Audit.Tests.ps1 | 86 +++++++++++++++++ 7 files changed, 249 insertions(+), 4 deletions(-) diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 249874d..550905f 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -234,11 +234,22 @@ namespace NTFSSecurity } // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there - // as a terminating error, the way the binder itself would have reported it for a typed parameter. + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. private void ValidateNamedValue(object value, string parameterName) { if (value == null) { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); return; } diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index d2ac020..a9d3a74 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -245,11 +245,22 @@ namespace NTFSSecurity } // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there - // as a terminating error, the way the binder itself would have reported it for a typed parameter. + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. private void ValidateNamedValue(object value, string parameterName) { if (value == null) { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); return; } diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index fa3f8da..560d333 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -238,11 +238,22 @@ namespace NTFSSecurity } // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there - // as a terminating error, the way the binder itself would have reported it for a typed parameter. + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. private void ValidateNamedValue(object value, string parameterName) { if (value == null) { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); return; } diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index 6d308e1..b1de294 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -249,11 +249,22 @@ namespace NTFSSecurity } // A bad value named on the command line is already bound when BeginProcessing runs, so it is reported there - // as a terminating error, the way the binder itself would have reported it for a typed parameter. + // as a terminating error, the way the binder itself would have reported it for a typed parameter. A named + // $null is bound too, whether it came from a literal $null, a variable, the alias -AccessControlType, + // splatting, or $PSDefaultParameterValues (MyInvocation.BoundParameters always has the canonical name), and + // is an error the same way; a $null that a piped object's property carries is not bound yet at this point, + // so it keeps the parameter's own default instead, as before. private void ValidateNamedValue(object value, string parameterName) { if (value == null) { + if (!MyInvocation.BoundParameters.ContainsKey(parameterName)) + { + return; + } + + var nullException = new ArgumentException(ParameterConversion.DescribeNull(parameterName)); + ThrowTerminatingError(new ErrorRecord(nullException, "InvalidParameterValue", ErrorCategory.InvalidArgument, value)); return; } diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index bb6241f..9adb60b 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -50,6 +50,19 @@ namespace NTFSSecurity } } + /// + /// Builds the message for a parameter whose value is explicitly null although it was bound on the command + /// line (a literal $null, a variable that holds $null, or a null entry of $PSDefaultParameterValues): "The + /// value for parameter -AccessType is null. Valid values: Allow, Deny." A piped property that is null is + /// not an error there: it means the property was not specified and the parameter's own default applies; + /// only the cmdlet knows, from MyInvocation.BoundParameters, whether a null came from such a named value. + /// + /// The name of the parameter, without the leading dash, for the message. + internal static string DescribeNull(string parameterName) + { + return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); + } + // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a // combination of them is written separated with commas. private static string DescribeValidValues(Type enumType) diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index af20c36..c3fd1c9 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1384,6 +1384,16 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } } + # R1-2: a named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must + # stay a terminating error, the same as a bad named value; only a $null that a piped object's property + # carries keeps the parameter's own default. + $accessNamedNullCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + $accessValidAccessTypeShapeCases = @( @{ Description = 'the exact name'; Value = 'Deny' } @{ Description = 'a different case'; Value = 'deny' } @@ -1612,6 +1622,98 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } + # R1-2: rc8 ended a named $null (for example a variable that misspells a property, such as -AccessType + # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepted it and + # silently kept the default instead. A named $null is now reported the same way as a bad named value. + Context 'A named $null is a terminating error' { + It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $accessNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $null + + $caughtId = $null + $caughtMessage = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + $caughtMessage = $_.Exception.Message + } + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: PowerShell's own binder already rejects a named $null before the cmdlet + # runs, with its own error id, unrelated to this fix; characterized here so that the row is not + # silently missing from the table. + $caughtId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $caughtId | Should -BeLike 'InvalidParameterValue,*' + $caughtMessage | Should -Match ([regex]::Escape($Parameter)) + $caughtMessage | Should -Match 'is null' + } + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should throw a terminating InvalidParameterValue error for a named $null of -AccessControlType (the alias), and change nothing' -ForEach @( + @{ Cmdlet = 'Add-NTFSAccess' } + @{ Cmdlet = 'Remove-NTFSAccess' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullAlias' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + + $caughtId = $null + try { + & $Cmdlet -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessControlType $null -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should throw a terminating InvalidParameterValue error for a $null of -AccessType from $PSDefaultParameterValues, and change nothing' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullDefault' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $PSDefaultParameterValues['Add-NTFSAccess:AccessType'] = $null + + $caughtId = $null + try { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + finally { + $PSDefaultParameterValues.Remove('Add-NTFSAccess:AccessType') + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + + It 'Should still give - its own default for a piped $null, unaffected by the named $null fix' -ForEach $accessNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedNullUnaffected' -Directory + $extra = @{ } + $extra[$Parameter] = $null + + [pscustomobject] (@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) | + & $Cmdlet -ErrorVariable pipedErrors -ErrorAction SilentlyContinue + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: a piped $null is the pre-existing binder validation error, unrelated to this fix. + $pipedErrors | Should -HaveCount 1 + $pipedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $pipedErrors | Should -BeNullOrEmpty + } + } + } + Context 'Choosing the parameter set with a mix of string and enum values' { It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index 046bfd4..fb2cb0d 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1054,6 +1054,16 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } } + # R1-2: a named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a + # terminating error, the same as a bad named value; only a $null that a piped object's property carries + # keeps the parameter's own default. + $auditNamedNullCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } + $auditValidAuditFlagsShapeCases = @( @{ Description = 'the exact name'; Value = 'Failure' } @{ Description = 'a different case'; Value = 'failure' } @@ -1294,6 +1304,82 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } + # R1-2: rc8 ended a named $null with a terminating ParameterBindingFailed; the object-typed parameter accepted + # it and silently kept the default instead. A named $null is now reported the same way as a bad named value. + Context 'A named $null is a terminating error' { + It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $auditNamedNullCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $null + + $caughtId = $null + $caughtMessage = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + $caughtMessage = $_.Exception.Message + } + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: PowerShell's own binder already rejects a named $null before the cmdlet + # runs, with its own error id, unrelated to this fix; characterized here so that the row is not + # silently missing from the table. + $caughtId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $caughtId | Should -BeLike 'InvalidParameterValue,*' + $caughtMessage | Should -Match ([regex]::Escape($Parameter)) + $caughtMessage | Should -Match 'is null' + } + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should throw a terminating InvalidParameterValue error for a $null of -AuditFlags from $PSDefaultParameterValues, and change nothing' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullDefault' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $PSDefaultParameterValues['Add-NTFSAudit:AuditFlags'] = $null + + $caughtId = $null + try { + Add-NTFSAudit -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + finally { + $PSDefaultParameterValues.Remove('Add-NTFSAudit:AuditFlags') + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + + It 'Should still give - its own default for a piped $null, unaffected by the named $null fix' -ForEach $auditNamedNullCases -Skip:(-not $canReadAudit) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedNullUnaffected' -Directory + $extra = @{ } + $extra[$Parameter] = $null + + [pscustomobject] (@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $extra) | + & $Cmdlet -ErrorVariable pipedErrors -ErrorAction SilentlyContinue + + if ($Parameter -eq 'AppliesTo') { + # AppliesTo is Mandatory: a piped $null is the pre-existing binder validation error, unrelated to this fix. + $pipedErrors | Should -HaveCount 1 + $pipedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' + } + else { + $pipedErrors | Should -BeNullOrEmpty + } + } + } + Context 'Choosing the parameter set with a mix of string and enum values' { It 'Should reject a piped object whose AppliesTo is combined with InheritanceFlags as with an AmbiguousParameterSet error' -ForEach $auditAmbiguousValueCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory From e75bc15953989b4b3409a8c041b5306d258e70ab Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:15:14 +0000 Subject: [PATCH 20/36] fix(access): complete quoted and wildcard words of the five parameters The completers for AccessType, AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo returned nothing for a quoted word (-AccessType "D) or a wildcard word (-AppliesTo *Only, -AccessType D*), which rc8's built-in enum-parameter completion still offered. PowerShell auto-closes an unterminated quote before it reaches the completer, so the word a completer receives for -AccessType "D is "D", not D. EnumArgumentCompletion.Complete now strips a leading quote and its matching trailing quote from the word before matching, matches the enumeration's names with WildcardPattern(word + "*", WildcardOptions.IgnoreCase) the way PowerShell's own enum completion does, and, when the word was quoted, returns each match in the same quote character (CompletionText 'FilesOnly' for a leading single quote). An unquoted, non-wildcard word is unaffected: appending "*" to it only turns an exact prefix match into a prefix-wildcard match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/EnumArgumentCompleters.cs | 27 ++++++++++++++++++---- Tests/ParameterSets.Access.Tests.ps1 | 31 ++++++++++++++++++++++++++ Tests/ParameterSets.Audit.Tests.ps1 | 27 ++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/NTFSSecurity/EnumArgumentCompleters.cs b/NTFSSecurity/EnumArgumentCompleters.cs index 87604d6..927d1ea 100644 --- a/NTFSSecurity/EnumArgumentCompleters.cs +++ b/NTFSSecurity/EnumArgumentCompleters.cs @@ -13,21 +13,40 @@ namespace NTFSSecurity internal static class EnumArgumentCompletion { /// - /// Returns the names of the enumeration that start with the word the user already typed, compared without - /// regard to case, in the order Enum.GetNames returns them. + /// Returns the names of the enumeration that match the word the user already typed, compared without + /// regard to case, in the order Enum.GetNames returns them. A leading quote is stripped before matching, + /// together with the matching trailing quote that PowerShell adds to a word it completes while the quote + /// is still open, and every match is returned quoted the same way, so that completing inside an open quote + /// keeps the command line valid. The word itself may already contain a wildcard, such as "*Only" or "D*", + /// the same as PowerShell completes a parameter of an enumeration type. /// /// The enumeration to complete the names of. /// The word the user already typed; empty or null completes every name. internal static IEnumerable Complete(Type enumType, string wordToComplete) { var word = wordToComplete ?? string.Empty; + char? quote = null; + + if (word.Length > 0 && (word[0] == '\'' || word[0] == '"')) + { + quote = word[0]; + word = word.Substring(1); + + if (word.Length > 0 && word[word.Length - 1] == quote.Value) + { + word = word.Substring(0, word.Length - 1); + } + } + + var pattern = new WildcardPattern(word + "*", WildcardOptions.IgnoreCase); var results = new List(); foreach (var name in Enum.GetNames(enumType)) { - if (name.StartsWith(word, StringComparison.OrdinalIgnoreCase)) + if (pattern.IsMatch(name)) { - results.Add(new CompletionResult(name, name, CompletionResultType.ParameterValue, name)); + var completionText = quote.HasValue ? quote.Value + name + quote.Value : name; + results.Add(new CompletionResult(completionText, name, CompletionResultType.ParameterValue, name)); } } diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index c3fd1c9..11189f1 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1420,6 +1420,17 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } } + + # R1-4: a quoted or a wildcard word completed in rc8 (a typed parameter of an enumeration); the first + # EnumArgumentCompleters.cs returned nothing for either shape. Each ExpectedName has no sibling name that + # also starts with it (ThisFolderOnly has no ThisFolderOnlyOneLevel), so the unquoted and quoted forms match + # it alone; the wildcard only needs to contain it among the matches. + $accessCompletionFormCases = @( + @{ Parameter = 'AccessType'; Word = 'D'; ExpectedName = 'Deny' } + @{ Parameter = 'InheritanceFlags'; Word = 'Container'; ExpectedName = 'ContainerInherit' } + @{ Parameter = 'PropagationFlags'; Word = 'Inh'; ExpectedName = 'InheritOnly' } + @{ Parameter = 'AppliesTo'; Word = 'ThisFolderO'; ExpectedName = 'ThisFolderOnly' } + ) } Context 'A bad value bound from the pipeline' { @@ -1753,6 +1764,26 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF It ' should declare - as [object]' -ForEach $accessCompleterCases { (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) } + + It 'Should complete Add-NTFSAccess - for the unquoted, single-quoted, double-quoted, and wildcard forms of ' -ForEach $accessCompletionFormCases { + $base = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} ' -f $Parameter + + $unquoted = $base + $Word + $r1 = [System.Management.Automation.CommandCompletion]::CompleteInput($unquoted, $unquoted.Length, $null) + @($r1.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + + $singleQuoted = $base + "'" + $Word + $r2 = [System.Management.Automation.CommandCompletion]::CompleteInput($singleQuoted, $singleQuoted.Length, $null) + @($r2.CompletionMatches.CompletionText) | Should -Contain ("'" + $ExpectedName + "'") + + $doubleQuoted = $base + '"' + $Word + $r3 = [System.Management.Automation.CommandCompletion]::CompleteInput($doubleQuoted, $doubleQuoted.Length, $null) + @($r3.CompletionMatches.CompletionText) | Should -Contain ('"' + $ExpectedName + '"') + + $wildcard = $base + '*' + $ExpectedName.Substring($ExpectedName.Length - 3) + $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) + @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + } } } diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index fb2cb0d..f3c6868 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1092,6 +1092,13 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } } + + # R1-4: AuditFlags is the one completer that ParameterSets.Access.Tests.ps1 does not already cover with the + # quoted and wildcard forms (it shares the same completer code for InheritanceFlags, PropagationFlags, and + # AppliesTo, so those are pinned there). + $auditCompletionFormCases = @( + @{ Parameter = 'AuditFlags'; Word = 'F'; ExpectedName = 'Failure' } + ) } Context 'A bad value bound from the pipeline' { @@ -1413,5 +1420,25 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF It ' should declare - as [object]' -ForEach $auditCompleterCases { (Get-Command -Name $Cmdlet).Parameters[$Parameter].ParameterType | Should -Be ([object]) } + + It 'Should complete Add-NTFSAudit - for the unquoted, single-quoted, double-quoted, and wildcard forms of ' -ForEach $auditCompletionFormCases { + $base = 'Add-NTFSAudit -Path C:\x -Account a -AccessRights Read -{0} ' -f $Parameter + + $unquoted = $base + $Word + $r1 = [System.Management.Automation.CommandCompletion]::CompleteInput($unquoted, $unquoted.Length, $null) + @($r1.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + + $singleQuoted = $base + "'" + $Word + $r2 = [System.Management.Automation.CommandCompletion]::CompleteInput($singleQuoted, $singleQuoted.Length, $null) + @($r2.CompletionMatches.CompletionText) | Should -Contain ("'" + $ExpectedName + "'") + + $doubleQuoted = $base + '"' + $Word + $r3 = [System.Management.Automation.CommandCompletion]::CompleteInput($doubleQuoted, $doubleQuoted.Length, $null) + @($r3.CompletionMatches.CompletionText) | Should -Contain ('"' + $ExpectedName + '"') + + $wildcard = $base + '*' + $ExpectedName.Substring($ExpectedName.Length - 3) + $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) + @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName + } } } From 7732fe57ac82f67c9427ac1297de49bb0c1857d5 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:52:45 +0000 Subject: [PATCH 21/36] fix(owner): pass on what a later command raises at the RestoreOwnerError record for the add and remove cmdlets Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit had the same flaw as the 13 handlers fixed before: when a later command of the pipeline throws exactly at the RestoreOwnerError record that InvokeAsOwner writes after a failed restore of the owner, the catch around the call reported the exception as the item's own AddAceError or RemoveAceError and went on with the next path, so the caller never saw it. The four handlers now check IsFromLaterCommand first, like the others, so all 17 handlers of item 7 of Decision 27 pass the exception on. The table of Tests\PipelineControl.Tests.ps1 gets two rows, for Add-NTFSAccess and Remove-NTFSAccess, which use the scenario of the inheritance cmdlets (a ChangePermissions deny for the user and an owner that only the Restore privilege can set back). The two audit cmdlets can't be pinned this way: Windows lets this elevated session write the SACL whatever the DACL says, so their action never fails to begin with. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/AccessCmdlets/AddAccess.cs | 6 +++ NTFSSecurity/AccessCmdlets/RemoveAccess.cs | 6 +++ NTFSSecurity/AuditCmdlets/AddAudit.cs | 6 +++ NTFSSecurity/AuditCmdlets/RemoveAudit.cs | 6 +++ Tests/PipelineControl.Tests.ps1 | 51 ++++++++++++++++------ 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/NTFSSecurity/AccessCmdlets/AddAccess.cs b/NTFSSecurity/AccessCmdlets/AddAccess.cs index 550905f..d3f93c3 100644 --- a/NTFSSecurity/AccessCmdlets/AddAccess.cs +++ b/NTFSSecurity/AccessCmdlets/AddAccess.cs @@ -190,6 +190,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs index a9d3a74..cb24b15 100644 --- a/NTFSSecurity/AccessCmdlets/RemoveAccess.cs +++ b/NTFSSecurity/AccessCmdlets/RemoveAccess.cs @@ -201,6 +201,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AuditCmdlets/AddAudit.cs b/NTFSSecurity/AuditCmdlets/AddAudit.cs index 560d333..d06f424 100644 --- a/NTFSSecurity/AuditCmdlets/AddAudit.cs +++ b/NTFSSecurity/AuditCmdlets/AddAudit.cs @@ -189,6 +189,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); continue; } diff --git a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs index b1de294..8aefd04 100644 --- a/NTFSSecurity/AuditCmdlets/RemoveAudit.cs +++ b/NTFSSecurity/AuditCmdlets/RemoveAudit.cs @@ -200,6 +200,12 @@ namespace NTFSSecurity } catch (Exception ex2) { + // Not what a later command raises, for example when it takes the RestoreOwnerError record. + if (IsFromLaterCommand(ex2)) + { + throw; + } + WriteError(new ErrorRecord(ex2, "RemoveAceError", ErrorCategory.WriteError, path)); continue; } diff --git a/Tests/PipelineControl.Tests.ps1 b/Tests/PipelineControl.Tests.ps1 index 89f97bd..01b117f 100644 --- a/Tests/PipelineControl.Tests.ps1 +++ b/Tests/PipelineControl.Tests.ps1 @@ -644,32 +644,32 @@ namespace NtfsSecurityTests { public class StandInForBreak : System.Management.A # and going on, or the caller never sees it. Each row denies the current user the right its own action needs (a # change denial for the writers, a read denial on Everyone for Get-FileHash2, which needs file data, not # permissions) and gives the item an owner that only the Restore privilege can set back, which the tests turn off; -# InvokeAsOwner/WriteChangesAsOwner then takes ownership, runs the action, and fails to restore the owner. Six of -# the thirteen handlers of item 7 can be pinned this way; the scenario and the "without a later command" assertion -# already exist for all six: ParameterSets.Inheritance.Tests.ps1 (Disable/Enable-NTFSAccessInheritance, +# InvokeAsOwner/WriteChangesAsOwner then takes ownership, runs the action, and fails to restore the owner. Eight of +# the seventeen handlers of item 7 can be pinned this way. The scenario and the "without a later command" assertion +# already exist for six of them: ParameterSets.Inheritance.Tests.ps1 (Disable/Enable-NTFSAccessInheritance, # Set-NTFSInheritance), PathErrors.Tests.ps1 (Clear-NTFSAccess -DisableInheritance), SecurityDescriptor.Tests.ps1 -# (Set-NTFSSecurityDescriptor), FileHash.Tests.ps1 (Get-FileHash2), so this table only adds the later-command -# assertion, not a duplicate of the existing one. +# (Set-NTFSSecurityDescriptor), FileHash.Tests.ps1 (Get-FileHash2); Add-NTFSAccess and Remove-NTFSAccess use the same +# scenario as the inheritance cmdlets. So this table only adds the later-command assertion, not a duplicate of the +# existing one. # -# NOT DONE for the other seven (the reason is the same Windows rule in both groups: taking ownership only bypasses a +# NOT DONE for the other nine (the reason is the same Windows rule in both groups: taking ownership only bypasses a # denial of ReadPermissions/ChangePermissions, never of a different right): Get-NTFSAccess, Get-NTFSOrphanedAccess, # Get-NTFSEffectiveAccess, and Get-NTFSInheritance read the security descriptor; InvokeAsOwner's own first statement, # FileSystemOwner.GetOwner, reads it too and needs the same ReadPermissions right the cmdlet's read needs, so a # denial that fails one fails the other and InvokeAsOwner never reaches the owner-restore step (verified: denying # ReadPermissions directly to the user reports ReadSecurityError with the plain UnauthorizedAccessException, not # RestoreOwnerError, and the owner is never taken; probes-wi2\p-restoreowner-read-vs-write.ps1, -# p-read-cmdlets-2.ps1). Clear-NTFSAudit, Disable-NTFSAuditInheritance, and Enable-NTFSAuditInheritance write the -# SACL, which Windows grants to this elevated session regardless of a DACL deny and regardless of the module's -# EnablePrivileges setting, so the action does not fail to begin with (verified with both a ChangePermissions deny -# and EnablePrivileges off: no error, the SACL is written, and the owner is left unchanged; -# probes-wi2\p-audit-restoreowner.ps1, p-audit-restoreowner-2.ps1). The integrator's four rows for -# Add/Remove-NTFSAccess/Audit need only the same four pieces of data (Prepare, Run, ItemErrorId, Untouched). +# p-read-cmdlets-2.ps1). Clear-NTFSAudit, Disable-NTFSAuditInheritance, Enable-NTFSAuditInheritance, Add-NTFSAudit, and +# Remove-NTFSAudit write the SACL, which Windows grants to this elevated session regardless of a DACL deny and +# regardless of the module's EnablePrivileges setting, so the action does not fail to begin with (verified with both +# a ChangePermissions deny and EnablePrivileges off: no error, the SACL is written, and the owner is left unchanged; +# probes-wi2\p-audit-restoreowner.ps1, p-audit-restoreowner-2.ps1). The handlers of all seventeen carry the check. Describe 'A later command that throws at the RestoreOwnerError record' { BeforeDiscovery { $holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' $laterCommandRestoreOwnerNames = @( 'Clear-NTFSAccess', 'Disable-NTFSAccessInheritance', 'Enable-NTFSAccessInheritance', 'Set-NTFSInheritance', - 'Set-NTFSSecurityDescriptor', 'Get-FileHash2' + 'Set-NTFSSecurityDescriptor', 'Get-FileHash2', 'Add-NTFSAccess', 'Remove-NTFSAccess' ) } @@ -762,6 +762,31 @@ Describe 'A later command that throws at the RestoreOwnerError record' { # raises ArgumentException for an empty generic List(Of T). Untouched = { param ($Context, $Emitted) (Test-Path -LiteralPath $Context.Next) -and $Emitted.Count -eq 0 } } + 'Add-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterAddBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterAddNext' + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Add-NTFSAccess -Path $Context.Blocked, $Context.Next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Continue } + ItemErrorId = 'AddAceError' + Untouched = { param ($Context, $Emitted) -not (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } + 'Remove-NTFSAccess' = @{ + Prepare = { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterRemoveBlocked' + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'LaterRemoveNext' + Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop + Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $currentUser = 'ChangePermissions' } + Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller + @{ Blocked = $blocked; Next = $next } + } + Run = { param ($Context) Remove-NTFSAccess -Path $Context.Blocked, $Context.Next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Continue } + ItemErrorId = 'RemoveAceError' + Untouched = { param ($Context, $Emitted) (Test-ExplicitEntry -Path $Context.Next -Account 'S-1-1-0') -and $Emitted.Count -eq 0 } + } } } From 32a411856a1618881f7389f4bee02cdecc007b57 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:37:51 +0000 Subject: [PATCH 22/36] docs(changelog): describe the changes of 5.0.0-rc9 Add the entries for the filter of Get-ChildItem2, the error of Get-NTFSOrphanedAccess, the position of -SecurityDescriptor of Set-NTFSSecurityDescriptor, the conversions from FileSecurity and DirectorySecurity (and what they mean for the cmdlets: Get-Acl objects now bind), the account list of the RemoveAll methods, the path overloads of the Add methods, the EnablePrivileges setting, the 17 owner-restore handlers, the piped values that the add and remove cmdlets could not convert, and the descriptors of those cmdlets that fail one by one. Say what the object type of the five parameters costs (a script block is no longer evaluated per object; an empty value of the other group makes an object ambiguous) and replace the sentence that said such a value is ignored. Each statement was checked by a probe against the published rc8 and the build of this branch, in both editions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b29616..2480980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,31 @@ The format is based on `FileNotFoundException`. A volume that is mounted into a folder, and a junction or symbolic link to another volume, probably fail the same way. Use a path that starts at the root of the volume +- **Breaking:** match `Get-ChildItem2 -Filter` once, in the cmdlet, with the + wildcard rules that `Get-ChildItem -Filter` applies in Windows PowerShell + 5.1, in both editions, and on the real name of an item, not its 8.3 short + name. `Report.*` also returns `Report`, which has no dot; a pattern that + ends in a dot, such as `Report.` or `*.`, returns the names without a dot; + `*.*.*` returns every item; and a question mark also matches nothing at + the end of a name and before a dot, so `a?` also returns `a`. An empty + `-Filter` returns every item, like `*`, instead of nothing, and a pattern + of more than 32,767 characters is a parameter error. In PowerShell 7, + `Get-ChildItem2` deliberately differs from its own `Get-ChildItem -Filter` + for a pattern that ends in a dot, unless an asterisk comes right before the + dot, and for a pattern of an asterisk, a dot, and one or two question marks +- **Breaking:** write a non-terminating error in `Get-NTFSOrphanedAccess` for + an item that it can't read for a reason other than a denied access, and + continue with the next item: `ReadSecurityError` with the category + `OpenError`; it wrote a warning. A script that reads the failure with + `-WarningVariable` needs `-ErrorVariable`, and `-ErrorAction Stop` now + stops at it +- Take `-SecurityDescriptor` of `Set-NTFSSecurityDescriptor` at position 1, + like the first positional parameter of the other cmdlets; it was at + position 2. A positional call isn't affected, because it is the only + positional parameter +- Document that `Get-NTFSEffectiveAccess` writes the error "Access is denied" + for a user account of the domain when the user who runs the cmdlet has no + domain credentials, such as a local standard user ### Deprecated @@ -121,6 +146,32 @@ The format is based on - Compare boxed privilege output values by their privilege and attributes; the object overload rejected privilege values and recursively compared an attributes enum instead +- Convert a `FileSecurity` or a `DirectorySecurity` object to + `FileSystemSecurity2` in the .NET API of `Security2.dll`: the conversions + ignored the descriptor and threw an exception for an empty path. The result + wraps the descriptor and has no item: `FullName` and `Name` are `$null`, + `Write()` throws an `InvalidOperationException`, and the entries, the + owner, the inheritance, and the effective access can be read; an inherited + entry has no source. PowerShell uses the conversion to bind + `-SecurityDescriptor`, so the cmdlets now accept the object that `Get-Acl` + returns, where they stopped with a parameter binding error. Such a + descriptor can't be written, so `Set-NTFSSecurityDescriptor` writes a + `WriteSdError` for it, and the audit cmdlets write a `ReadSecurityError` + for one that has no audit entries +- **Breaking:** fix `RemoveFileSystemAccessRuleAll` and + `RemoveFileSystemAuditRuleAll` of the .NET API, which ignored the list of + accounts and removed every explicit entry; they remove only the entries of + the listed accounts, and an empty list removes none +- **Breaking:** fix the overloads of `AddFileSystemAccessRule` and + `AddFileSystemAuditRule` of the .NET API that take a path and a list of + accounts, which did nothing until the caller enumerated the result; they + write at once, and an exception reaches the caller of the method +- Fix a module setting `EnablePrivileges` that is missing or not `$true` or + `$false`, for example after + `(Get-Module NTFSSecurity).PrivateData['EnablePrivileges'] = 'false'`: the + cmdlets that use privileges failed with an `InvalidCastException` or a + `NullReferenceException`; they stop with one error, + `EnablePrivilegesSettingInvalid`, which names the setting - Fix `Clear-NTFSAccess -DisableInheritance` and `Set-NTFSSecurityDescriptor`, which reported a `RestoreOwnerError` for an owner that had not changed: after they took ownership of an item that the user owned already and left a @@ -150,6 +201,15 @@ The format is based on exception of a later command when that command took the debug message after the enabling, for example with `5>&1 | Select-Object -First 2`; they now disable the privilege and pass the exception on +- Fix the 17 cmdlets that take ownership of an item to retry a denied read or + change and then set the previous owner back: the `Add-`, `Remove-`, and + `Clear-` cmdlets for access and audit entries, `Get-NTFSAccess`, + `Get-NTFSOrphanedAccess`, `Get-NTFSEffectiveAccess`, the six inheritance + cmdlets, `Get-FileHash2`, and `Set-NTFSSecurityDescriptor`. When a later + command in the pipeline threw at the `RestoreOwnerError` that setting the + owner back can write, they reported the exception as the error of the item + and went on with the next one, so that the caller never saw it; they now + pass it on - Fix `Get-ChildItem2 -Filter`, which read a bracket as the start of a character class, so that it did not return a file with brackets in its name, such as `Report[1].txt`, for that name; only `*` and `?` are wildcards. A @@ -251,14 +311,40 @@ The format is based on object that has `AppliesTo` and `InheritanceFlags` or `PropagationFlags` now fails with `AmbiguousParameterSet`, like the same parameters on the command line, and so does an object that has both `Path` and - `SecurityDescriptor`, where the path won before. An `AppliesTo` that is - `$null` fails to bind for that object (a row of a CSV file that has fewer - fields than its header gives `$null`). A value that PowerShell can't + `SecurityDescriptor`, where the path won before. The empty cell of a CSV + file, `$null`, and `DBNull` count as a value of the other group here, + because PowerShell binds them to the parameters of both groups: a file + that has columns of both groups fails for a row that leaves one of them + empty, so give such a file the columns of one group only. An `AppliesTo` + that is `$null` fails to bind for that object (a row of a CSV file that has + fewer fields than its header gives `$null`). A value that PowerShell can't convert to a scope, such as the empty cell of a CSV file or a misspelled - name, is ignored, and the object is handled as one without `AppliesTo`: it - gets its own flags, or the default scope if it has none. A call with + name, is an error for that object, see the next entry. A call with neither `-Path` nor `-SecurityDescriptor` now fails with `AmbiguousParameterSet` instead of a missing-parameter error for `-Path` +- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and + `Remove-NTFSAudit`, which silently used the default when a piped object had + a value of `AccessType` (`AuditFlags` for the audit cmdlets), + `InheritanceFlags`, `PropagationFlags`, or `AppliesTo` that PowerShell + could not convert, such as a misspelled name or the empty cell of a CSV + file: a typo for `Deny` created an `Allow` entry, and a misspelled scope + used the default scope, without a message. The cmdlets now write the + non-terminating error `InvalidParameterValue` for that object, change + nothing for it, and go on with the next one; a blank value is such an + error too, where it used the default. **Breaking:** the five parameters + have the type `Object`, because the cmdlets convert the values themselves. + A bad value that you name on the command line, and a named `$null`, still + stop the command, but with the error id `InvalidParameterValue`, not the id + of a parameter binding error. A script block, such as + `-AccessType { 'Deny' }`, is no longer evaluated for every piped object and + is a bad value. `Get-Help` shows the type `Object`, and tab completion + still offers the valid names +- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and + `Remove-NTFSAudit` with `-SecurityDescriptor`, which ended the whole + command with a terminating error when the change of one descriptor failed, + such as for a deny entry without rights; they write the non-terminating + error `AddAceError` or `RemoveAceError` for that descriptor and go on with + the next one, as they do for a path - Fix `Get-NTFSEffectiveAccess`: `-ExcludeNoneAccessEntries` now leaves out items without access, the cmdlet uses the current location when `-Path` is omitted, and `-SecurityDescriptor` returns the effective access of the From 140c790c1196061234a62d6ad6b734ca931ac7df Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:48:15 +0000 Subject: [PATCH 23/36] docs(access): say what a blank, null, or script block value of a property does, and pin it R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error for every shape. That is wrong when the blank, $null, or DBNull value belongs to the group that -AppliesTo did not select: PowerShell counts a property that is present, even blank, as offered for choosing the parameter set, the same as a real value, so AppliesTo together with a blank InheritanceFlags or PropagationFlags (for example a short CSV row, whose missing trailing columns Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the entry. The pages now say that a CSV file should have the columns of only one group, that mixing both groups fails with AmbiguousParameterSet, that a literal $null named or piped for -AppliesTo itself fails with its own, unrelated, pre-existing error because -AppliesTo is mandatory, and keep the sentence that a bad value within the chosen group is InvalidParameterValue. R1-3 (minor): the pages now also say that a script block is a plain value for these parameters and is not evaluated once for each piped object the way rc8 evaluated it, so a named script block is a terminating InvalidParameterValue error. Neither finding needed a code change; both already read this way. Pinned with new table-driven tests, over the four cmdlets and their Path and SD parameter sets, for AppliesTo valid combined with a $null, an empty string, or DBNull InheritanceFlags or PropagationFlags and the reverse, and for a named script block of each of the five parameters: the error id the binder already gives, nothing changed, and, where applicable, the next piped object still processed. Confirmed unchanged against the pre-R1-2/R1-4 build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Add-NTFSAccess.md | 2 +- Docs/Cmdlets/Add-NTFSAudit.md | 2 +- Docs/Cmdlets/Remove-NTFSAccess.md | 2 +- Docs/Cmdlets/Remove-NTFSAudit.md | 2 +- Tests/ParameterSets.Access.Tests.ps1 | 98 +++++++++++++++++++++++ Tests/ParameterSets.Audit.Tests.ps1 | 112 +++++++++++++++++++++++++++ 6 files changed, 214 insertions(+), 4 deletions(-) diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 3826c4e..7829a40 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -46,7 +46,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DAC `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see [Concepts](../Concepts.md). -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index ec2410b..b59f886 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -46,7 +46,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. -A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index 195e0b9..20827da 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -48,7 +48,7 @@ Only the specified rights are taken away: when an entry grants more than `-Acces Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. ## EXAMPLES diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index 78e4516..2921ce9 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -46,7 +46,7 @@ The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access cont Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. -In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` that PowerShell can't convert to its type, such as the blank cell of a CSV file or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 11189f1..f1b1cda 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1431,6 +1431,44 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Parameter = 'PropagationFlags'; Word = 'Inh'; ExpectedName = 'InheritOnly' } @{ Parameter = 'AppliesTo'; Word = 'ThisFolderO'; ExpectedName = 'ThisFolderOnly' } ) + + # R1-1: because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value + # of a property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the + # Simple group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) + # offers both groups and is AmbiguousParameterSet -- not InvalidParameterValue, which is what the docs + # wrongly claimed for every shape. The reverse (AppliesTo blank, a Complex property valid) is + # AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal $null of it (unlike '' or + # DBNull) is rejected earlier by the binder's own Mandatory check, unrelated to and unchanged by this + # finding. Covers the four cmdlets' Path and SD sets; the Audit half of the four cmdlets is pinned in + # ParameterSets.Audit.Tests.ps1. + $accessAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + foreach ($targetKind in 'Path', 'SD') { + foreach ($shape in 'a $null', 'an empty string', 'DBNull') { + $blankValue = switch ($shape) { 'a $null' { $null }; 'an empty string' { '' }; 'DBNull' { [System.DBNull]::Value } } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo valid, InheritanceFlags blank'; AppliesToIsBlank = $false + ExpectedErrorId = 'AmbiguousParameterSet' + } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo blank, PropagationFlags valid'; AppliesToIsBlank = $true + ExpectedErrorId = $(if ($shape -eq 'a $null') { 'ParameterArgumentValidationErrorNullNotAllowed' } else { 'AmbiguousParameterSet' }) + } + } + } + } + + # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped + # object, no longer applies), so it fails the same way any other value of the wrong shape does: a + # terminating InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is + # processed. + $accessScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { + @{ Cmdlet = $cmdlet; Parameter = 'AccessType' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } } Context 'A bad value bound from the pipeline' { @@ -1751,6 +1789,66 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } + # R1-1: rc8 created the entry for every shape below; the object-typed parameters make a $null, an empty + # string, or DBNull value of a property of the other group count as present too, so PowerShell cannot resolve + # the parameter set and nothing is changed. + Context 'A blank value of the other groups property' { + It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $accessAmbiguousOtherGroupCases { + $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory + $before = (Get-Acl -LiteralPath $bad).Sddl + + $badTarget = if ($TargetKind -eq 'Path') { @{ Path = $bad } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $bad -ErrorAction Stop) } } + $nextTarget = if ($TargetKind -eq 'Path') { @{ Path = $next } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $next -ErrorAction Stop) } } + + $badExtra = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } + if ($AppliesToIsBlank) { + $badExtra['AppliesTo'] = $BlankValue + $badExtra['PropagationFlags'] = 'None' + } + else { + $badExtra['AppliesTo'] = 'ThisFolderOnly' + $badExtra['InheritanceFlags'] = $BlankValue + } + + $rows = @( + [pscustomobject] ($badTarget + $badExtra) + [pscustomobject] ($nextTarget + @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' }) + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $pipelineErrors | Should -HaveCount 1 + $pipelineErrors[0].FullyQualifiedErrorId | Should -BeLike "$ExpectedErrorId,*" + (Get-Acl -LiteralPath $bad).Sddl | Should -BeExactly $before + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + + # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped + # object, no longer applies), so ValidateNamedValue in BeginProcessing rejects it the same as any other bad + # value, before any piped object is processed. + Context 'A named script block is a terminating error' { + It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $accessScriptBlockCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = { 'Deny' } + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + Context 'Tab completion and the parameter type' { It ' - should complete exactly the names of its enumeration' -ForEach $accessCompleterCases { $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index f3c6868..d43655e 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1099,6 +1099,44 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF $auditCompletionFormCases = @( @{ Parameter = 'AuditFlags'; Word = 'F'; ExpectedName = 'Failure' } ) + + # R1-1: because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value + # of a property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the + # Simple group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) + # offers both groups and is AmbiguousParameterSet -- not InvalidParameterValue, which is what the docs + # wrongly claimed for every shape. The reverse (AppliesTo blank, a Complex property valid) is + # AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal $null of it (unlike '' or + # DBNull) is rejected earlier by the binder's own Mandatory check, unrelated to and unchanged by this + # finding. Covers the four cmdlets' Path and SD sets; the Access half is pinned in + # ParameterSets.Access.Tests.ps1. + $auditAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + foreach ($targetKind in 'Path', 'SD') { + foreach ($shape in 'a $null', 'an empty string', 'DBNull') { + $blankValue = switch ($shape) { 'a $null' { $null }; 'an empty string' { '' }; 'DBNull' { [System.DBNull]::Value } } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo valid, InheritanceFlags blank'; AppliesToIsBlank = $false + ExpectedErrorId = 'AmbiguousParameterSet' + } + @{ + Cmdlet = $cmdlet; TargetKind = $targetKind; Shape = $shape; BlankValue = $blankValue + Direction = 'AppliesTo blank, PropagationFlags valid'; AppliesToIsBlank = $true + ExpectedErrorId = $(if ($shape -eq 'a $null') { 'ParameterArgumentValidationErrorNullNotAllowed' } else { 'AmbiguousParameterSet' }) + } + } + } + } + + # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped + # object, no longer applies), so it fails the same way any other value of the wrong shape does: a + # terminating InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is + # processed. + $auditScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo' } + } } Context 'A bad value bound from the pipeline' { @@ -1407,6 +1445,80 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } + # R1-1: rc8 created the entry for every shape below; the object-typed parameters make a $null, an empty + # string, or DBNull value of a property of the other group count as present too, so PowerShell cannot resolve + # the parameter set and nothing is changed. The conversion/parameter-set failure happens before the cmdlet + # ever touches the SACL, so the core assertion (the error id, and that the bad object's audit entries are + # unchanged) runs in every configuration; only the "good next object succeeded" part needs the Security + # privilege, the same way the rest of this file does. + Context 'A blank value of the other groups property' { + It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $auditAmbiguousOtherGroupCases { + $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory + $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $bad } + + $badTarget = if ($TargetKind -eq 'Path') { @{ FullName = $bad } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $bad -ErrorAction Stop) } } + $nextTarget = if ($TargetKind -eq 'Path') { @{ FullName = $next } } else { @{ SecurityDescriptor = (Get-NTFSSecurityDescriptor -Path $next -ErrorAction Stop) } } + + $badExtra = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } + if ($AppliesToIsBlank) { + $badExtra['AppliesTo'] = $BlankValue + $badExtra['PropagationFlags'] = 'None' + } + else { + $badExtra['AppliesTo'] = 'ThisFolderOnly' + $badExtra['InheritanceFlags'] = $BlankValue + } + + $rows = @( + [pscustomobject] ($badTarget + $badExtra) + [pscustomobject] ($nextTarget + @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' }) + ) + + $result = @($rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue) + + $badErrors = @($pipelineErrors | Where-Object -FilterScript { + ($TargetKind -eq 'Path' -and $_.TargetObject.FullName -eq $bad) -or + ($TargetKind -eq 'SD' -and $_.TargetObject.SecurityDescriptor -eq $badTarget['SecurityDescriptor']) + }) + $badErrors | Should -HaveCount 1 + $badErrors[0].FullyQualifiedErrorId | Should -BeLike "$ExpectedErrorId,*" + if ($canReadAudit) { + Get-TestAuditSddl -Path $bad | Should -BeExactly $before + $pipelineErrors | Should -HaveCount 1 + $result | Should -Not -BeNullOrEmpty + $result | ForEach-Object -Process { $_.FullName | Should -Be $next } + } + } + } + + # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped + # object, no longer applies), so ValidateNamedValue in BeginProcessing rejects it the same as any other bad + # value, before any piped object is processed and before the cmdlet ever touches the SACL. The terminating + # error itself needs no Security privilege; reading the SACL back to confirm nothing changed does, the same + # way the rest of this file does. + Context 'A named script block is a terminating error' { + It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $auditScriptBlockCases { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory + $before = if ($canReadAudit) { Get-TestAuditSddl -Path $folder } + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = { 'Deny' } + + $caughtId = $null + try { + & $Cmdlet @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + if ($canReadAudit) { + Get-TestAuditSddl -Path $folder | Should -BeExactly $before + } + } + } + Context 'Tab completion and the parameter type' { It ' - should complete exactly the names of its enumeration' -ForEach $auditCompleterCases { $inputScript = '{0} -{1} ' -f $Cmdlet, $Parameter From 68eab76867265c4fbe54c05fe25e59ca3845ae27 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 14:58:36 +0000 Subject: [PATCH 24/36] test: describe the new tests by behavior, not by review finding Remove the labels of review findings and the references to probe scripts and reports that are not in the repository from the comments of the tests that rc9 adds, and say what each test pins in words that stand alone. Only comments and the name of one Context in each of two files change; no assertion does. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Tests/ItemCmdlets.Tests.ps1 | 6 +-- Tests/ObjectApis.Tests.ps1 | 8 ++-- Tests/ParameterSets.Access.Tests.ps1 | 53 +++++++++++++-------------- Tests/ParameterSets.Audit.Tests.ps1 | 55 +++++++++++++--------------- Tests/PipelineControl.Tests.ps1 | 43 ++++++++++------------ 5 files changed, 78 insertions(+), 87 deletions(-) diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index e610a82..721a027 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -350,9 +350,9 @@ Describe 'Get-ChildItem2' { # last dot before the dot token and the question mark account for the final segment. The 8.3-compliant # names of the truth table above have at most one dot, where the first dot and the last dot are the same # position, so they cannot tell the two rules apart; this fixture uses names with two or three dots - # instead, including the realistic ones from the WI. Expected values are pinned with the real name of each - # item, confirmed with `fsutil file setshortname` so that a short name the volume might otherwise assign - # cannot also produce a match. + # instead, including realistic ones such as archive.tar.gz and jquery.min.js. Expected values are pinned + # with the real name of each item, confirmed with `fsutil file setshortname` so that a short name the volume + # might otherwise assign cannot also produce a match. It 'Should match -Filter "" on a name with two or more dots the way Windows PowerShell 5.1 does' -ForEach @( @{ Filter = '*.?'; Expected = @('a.b.c', 'v1.2.3', 'x.y.z.w') } @{ Filter = '*.??'; Expected = @('a.b.c', 'a.b.cd', 'archive.tar.gz', 'index.d.ts', 'jquery.min.js', 'my.file.tx', 'v1.2.3', 'x.y.z.w') } diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index 2312e85..5b39cc9 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -1026,9 +1026,10 @@ Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySec } } -# Review finding: PowerShell's binder uses the fixed conversion of 3.1 for every -SecurityDescriptor parameter, so a -# descriptor that Get-Acl returns, never meant for these cmdlets, now binds where it used to be a binding error. These -# pin the four cmdlets the review named; the one-line probes of the others are in the final report, not here. +# PowerShell's binder uses the conversion from FileSecurity and DirectorySecurity for every -SecurityDescriptor +# parameter, so a descriptor that Get-Acl returns, never meant for these cmdlets, binds where it used to be a binding +# error. The descriptor has no item and no audit section unless Get-Acl read one. These pin a reader of the access +# entries, a reader of the audit entries, the inheritance reader, and the writer. Describe 'Cmdlets with a security descriptor that Get-Acl returns' { BeforeAll { $plainFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAclPlain' @@ -1082,6 +1083,7 @@ Describe 'Cmdlets with a security descriptor that Get-Acl returns' { (Get-Acl -LiteralPath $plainFile).Sddl | Should -BeExactly $before } } + Describe 'RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll honor the account list' { BeforeAll { $allow = [System.Security.AccessControl.AccessControlType]::Allow diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index f1b1cda..75481c0 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1384,7 +1384,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } } - # R1-2: a named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must + # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must # stay a terminating error, the same as a bad named value; only a $null that a piped object's property # carries keeps the parameter's own default. $accessNamedNullCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { @@ -1421,10 +1421,10 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } } - # R1-4: a quoted or a wildcard word completed in rc8 (a typed parameter of an enumeration); the first - # EnumArgumentCompleters.cs returned nothing for either shape. Each ExpectedName has no sibling name that - # also starts with it (ThisFolderOnly has no ThisFolderOnlyOneLevel), so the unquoted and quoted forms match - # it alone; the wildcard only needs to contain it among the matches. + # A quoted or a wildcard word completed while these parameters were typed as enumerations, so the completers + # must offer the names for either shape too. Each ExpectedName has no sibling name that also starts with it + # (ThisFolderOnly has no ThisFolderOnlyOneLevel), so the unquoted and quoted forms match it alone; the + # wildcard only needs to contain it among the matches. $accessCompletionFormCases = @( @{ Parameter = 'AccessType'; Word = 'D'; ExpectedName = 'Deny' } @{ Parameter = 'InheritanceFlags'; Word = 'Container'; ExpectedName = 'ContainerInherit' } @@ -1432,15 +1432,13 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Parameter = 'AppliesTo'; Word = 'ThisFolderO'; ExpectedName = 'ThisFolderOnly' } ) - # R1-1: because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value - # of a property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the - # Simple group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) - # offers both groups and is AmbiguousParameterSet -- not InvalidParameterValue, which is what the docs - # wrongly claimed for every shape. The reverse (AppliesTo blank, a Complex property valid) is - # AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal $null of it (unlike '' or - # DBNull) is rejected earlier by the binder's own Mandatory check, unrelated to and unchanged by this - # finding. Covers the four cmdlets' Path and SD sets; the Audit half of the four cmdlets is pinned in - # ParameterSets.Audit.Tests.ps1. + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a + # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple + # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers + # both groups and is AmbiguousParameterSet, not InvalidParameterValue. The reverse (AppliesTo blank, a + # Complex property valid) is AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal + # $null of it (unlike '' or DBNull) is rejected earlier by the binder's own Mandatory check. Covers the + # Path and SD sets of the access cmdlets; the audit cmdlets are pinned in ParameterSets.Audit.Tests.ps1. $accessAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { foreach ($targetKind in 'Path', 'SD') { foreach ($shape in 'a $null', 'an empty string', 'DBNull') { @@ -1459,10 +1457,9 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } - # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped - # object, no longer applies), so it fails the same way any other value of the wrong shape does: a - # terminating InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is - # processed. + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped + # object), so it fails the same way any other value of the wrong shape does: a terminating + # InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is processed. $accessScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { @{ Cmdlet = $cmdlet; Parameter = 'AccessType' } @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } @@ -1671,9 +1668,9 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } - # R1-2: rc8 ended a named $null (for example a variable that misspells a property, such as -AccessType - # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepted it and - # silently kept the default instead. A named $null is now reported the same way as a bad named value. + # A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType + # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepts it, so the + # cmdlet must not keep the default silently. A named $null is reported the same way as a bad named value. Context 'A named $null is a terminating error' { It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $accessNamedNullCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory @@ -1789,10 +1786,10 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } - # R1-1: rc8 created the entry for every shape below; the object-typed parameters make a $null, an empty - # string, or DBNull value of a property of the other group count as present too, so PowerShell cannot resolve - # the parameter set and nothing is changed. - Context 'A blank value of the other groups property' { + # A typed parameter skips a $null, an empty string, or DBNull value of a property, and the entry was created for + # every shape below; the object-typed parameters count such a value of a property of the other group as present + # too, so PowerShell cannot resolve the parameter set and nothing is changed. + Context 'A blank value of a property of the other group' { It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $accessAmbiguousOtherGroupCases { $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory @@ -1826,9 +1823,9 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } - # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped - # object, no longer applies), so ValidateNamedValue in BeginProcessing rejects it the same as any other bad - # value, before any piped object is processed. + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped object), + # so ValidateNamedValue in BeginProcessing rejects it the same as any other bad value, before any piped object + # is processed. Context 'A named script block is a terminating error' { It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $accessScriptBlockCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index d43655e..d892875 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1054,7 +1054,7 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } } - # R1-2: a named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a + # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a # terminating error, the same as a bad named value; only a $null that a piped object's property carries # keeps the parameter's own default. $auditNamedNullCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { @@ -1093,22 +1093,20 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; EnumTypeName = 'Security2.ApplyTo' } } - # R1-4: AuditFlags is the one completer that ParameterSets.Access.Tests.ps1 does not already cover with the + # AuditFlags is the one completer that ParameterSets.Access.Tests.ps1 does not already cover with the # quoted and wildcard forms (it shares the same completer code for InheritanceFlags, PropagationFlags, and # AppliesTo, so those are pinned there). $auditCompletionFormCases = @( @{ Parameter = 'AuditFlags'; Word = 'F'; ExpectedName = 'Failure' } ) - # R1-1: because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value - # of a property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the - # Simple group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) - # offers both groups and is AmbiguousParameterSet -- not InvalidParameterValue, which is what the docs - # wrongly claimed for every shape. The reverse (AppliesTo blank, a Complex property valid) is - # AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal $null of it (unlike '' or - # DBNull) is rejected earlier by the binder's own Mandatory check, unrelated to and unchanged by this - # finding. Covers the four cmdlets' Path and SD sets; the Access half is pinned in - # ParameterSets.Access.Tests.ps1. + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a + # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple + # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers + # both groups and is AmbiguousParameterSet, not InvalidParameterValue. The reverse (AppliesTo blank, a + # Complex property valid) is AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal + # $null of it (unlike '' or DBNull) is rejected earlier by the binder's own Mandatory check. Covers the + # Path and SD sets of the audit cmdlets; the access cmdlets are pinned in ParameterSets.Access.Tests.ps1. $auditAmbiguousOtherGroupCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { foreach ($targetKind in 'Path', 'SD') { foreach ($shape in 'a $null', 'an empty string', 'DBNull') { @@ -1127,10 +1125,9 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } - # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped - # object, no longer applies), so it fails the same way any other value of the wrong shape does: a - # terminating InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is - # processed. + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped + # object), so it fails the same way any other value of the wrong shape does: a terminating + # InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is processed. $auditScriptBlockCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags' } @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags' } @@ -1349,8 +1346,9 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } - # R1-2: rc8 ended a named $null with a terminating ParameterBindingFailed; the object-typed parameter accepted - # it and silently kept the default instead. A named $null is now reported the same way as a bad named value. + # A typed parameter ends a named $null with a terminating ParameterBindingFailed; the object-typed parameter + # accepts it, so the cmdlet must not keep the default silently. A named $null is reported the same way as a bad + # named value. Context 'A named $null is a terminating error' { It 'Should throw a terminating error for a named $null of -, and change nothing' -ForEach $auditNamedNullCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory @@ -1445,13 +1443,13 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } - # R1-1: rc8 created the entry for every shape below; the object-typed parameters make a $null, an empty - # string, or DBNull value of a property of the other group count as present too, so PowerShell cannot resolve - # the parameter set and nothing is changed. The conversion/parameter-set failure happens before the cmdlet - # ever touches the SACL, so the core assertion (the error id, and that the bad object's audit entries are - # unchanged) runs in every configuration; only the "good next object succeeded" part needs the Security - # privilege, the same way the rest of this file does. - Context 'A blank value of the other groups property' { + # A typed parameter skips a $null, an empty string, or DBNull value of a property, and the entry was created for + # every shape below; the object-typed parameters count such a value of a property of the other group as present + # too, so PowerShell cannot resolve the parameter set and nothing is changed. The conversion/parameter-set + # failure happens before the cmdlet ever touches the SACL, so the core assertion (the error id, and that the bad + # object's audit entries are unchanged) runs in every configuration; only the "good next object succeeded" part + # needs the Security privilege, the same way the rest of this file does. + Context 'A blank value of a property of the other group' { It 'Should reject () for () with the error the binder gives, change nothing, and still process the next object' -ForEach $auditAmbiguousOtherGroupCases { $bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory $next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory @@ -1492,11 +1490,10 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF } } - # R1-3: with the object type, a script block is a plain value (rc8's delay-bind evaluation, once per piped - # object, no longer applies), so ValidateNamedValue in BeginProcessing rejects it the same as any other bad - # value, before any piped object is processed and before the cmdlet ever touches the SACL. The terminating - # error itself needs no Security privilege; reading the SACL back to confirm nothing changed does, the same - # way the rest of this file does. + # With the object type, a script block is a plain value (a typed parameter evaluates it once per piped object), + # so ValidateNamedValue in BeginProcessing rejects it the same as any other bad value, before any piped object + # is processed and before the cmdlet ever touches the SACL. The terminating error itself needs no Security + # privilege; reading the SACL back to confirm nothing changed does, the same way the rest of this file does. Context 'A named script block is a terminating error' { It 'Should throw a terminating InvalidParameterValue error for a named script block value of -, and change nothing' -ForEach $auditScriptBlockCases { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory diff --git a/Tests/PipelineControl.Tests.ps1 b/Tests/PipelineControl.Tests.ps1 index 01b117f..68c4c7b 100644 --- a/Tests/PipelineControl.Tests.ps1 +++ b/Tests/PipelineControl.Tests.ps1 @@ -637,33 +637,28 @@ namespace NtfsSecurityTests { public class StandInForBreak : System.Management.A } } -# InvokeAsOwner (and Set-NTFSSecurityDescriptor's WriteChangesAsOwner) take ownership, run the action, and in a +# InvokeAsOwner (and WriteChangesAsOwner of Set-NTFSSecurityDescriptor) take ownership, run the action, and in a # finally restore the previous owner, writing a RestoreOwnerError through WriteError when that restore fails. # WriteError runs the later commands of the pipeline and notes what they raise (IsFromLaterCommand, above); the catch -# around the InvokeAsOwner/WriteChangesAsOwner call must pass that on instead of reporting it as the item's own error -# and going on, or the caller never sees it. Each row denies the current user the right its own action needs (a -# change denial for the writers, a read denial on Everyone for Get-FileHash2, which needs file data, not -# permissions) and gives the item an owner that only the Restore privilege can set back, which the tests turn off; -# InvokeAsOwner/WriteChangesAsOwner then takes ownership, runs the action, and fails to restore the owner. Eight of -# the seventeen handlers of item 7 can be pinned this way. The scenario and the "without a later command" assertion -# already exist for six of them: ParameterSets.Inheritance.Tests.ps1 (Disable/Enable-NTFSAccessInheritance, -# Set-NTFSInheritance), PathErrors.Tests.ps1 (Clear-NTFSAccess -DisableInheritance), SecurityDescriptor.Tests.ps1 -# (Set-NTFSSecurityDescriptor), FileHash.Tests.ps1 (Get-FileHash2); Add-NTFSAccess and Remove-NTFSAccess use the same -# scenario as the inheritance cmdlets. So this table only adds the later-command assertion, not a duplicate of the -# existing one. +# around the call must pass that on instead of reporting it as the item's own error and going on, or the caller never +# sees it. Each row denies the current user the right its own action needs (a change denial for the writers, a read +# denial on Everyone for Get-FileHash2, which needs file data, not permissions) and gives the item an owner that only +# the Restore privilege can set back, which the tests turn off; the cmdlet then takes ownership, runs the action, and +# fails to restore the owner. Eight of the seventeen handlers can be pinned this way. The scenario and the "without a +# later command" assertion already exist for most of them in the test files of their cmdlets +# (ParameterSets.Inheritance.Tests.ps1, PathErrors.Tests.ps1, SecurityDescriptor.Tests.ps1, FileHash.Tests.ps1), and +# Add-NTFSAccess and Remove-NTFSAccess use the scenario of the inheritance cmdlets, so this table only adds the +# later-command assertion. # -# NOT DONE for the other nine (the reason is the same Windows rule in both groups: taking ownership only bypasses a -# denial of ReadPermissions/ChangePermissions, never of a different right): Get-NTFSAccess, Get-NTFSOrphanedAccess, -# Get-NTFSEffectiveAccess, and Get-NTFSInheritance read the security descriptor; InvokeAsOwner's own first statement, -# FileSystemOwner.GetOwner, reads it too and needs the same ReadPermissions right the cmdlet's read needs, so a -# denial that fails one fails the other and InvokeAsOwner never reaches the owner-restore step (verified: denying -# ReadPermissions directly to the user reports ReadSecurityError with the plain UnauthorizedAccessException, not -# RestoreOwnerError, and the owner is never taken; probes-wi2\p-restoreowner-read-vs-write.ps1, -# p-read-cmdlets-2.ps1). Clear-NTFSAudit, Disable-NTFSAuditInheritance, Enable-NTFSAuditInheritance, Add-NTFSAudit, and -# Remove-NTFSAudit write the SACL, which Windows grants to this elevated session regardless of a DACL deny and -# regardless of the module's EnablePrivileges setting, so the action does not fail to begin with (verified with both -# a ChangePermissions deny and EnablePrivileges off: no error, the SACL is written, and the owner is left unchanged; -# probes-wi2\p-audit-restoreowner.ps1, p-audit-restoreowner-2.ps1). The handlers of all seventeen carry the check. +# The other nine have no row, for one Windows rule: taking ownership only bypasses a denial of +# ReadPermissions/ChangePermissions, never of a different right. Get-NTFSAccess, Get-NTFSOrphanedAccess, +# Get-NTFSEffectiveAccess, and Get-NTFSInheritance read the security descriptor, and so does the first statement of +# InvokeAsOwner (FileSystemOwner.GetOwner), which needs the same ReadPermissions right: a denial that fails one fails +# the other, so the owner-restore step is never reached (denying ReadPermissions reports a ReadSecurityError with the +# plain UnauthorizedAccessException, and the owner is never taken). Clear-NTFSAudit, Disable-NTFSAuditInheritance, +# Enable-NTFSAuditInheritance, Add-NTFSAudit, and Remove-NTFSAudit write the SACL, which Windows grants to this +# elevated session whatever the DACL says and whatever the EnablePrivileges setting is, so the action does not fail +# to begin with (the SACL is written and the owner is left unchanged). The handlers of all seventeen carry the check. Describe 'A later command that throws at the RestoreOwnerError record' { BeforeDiscovery { $holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' From 6d98b68d955a5ce5fa9f7f85668d72c7baf01a7f Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 15:04:09 +0000 Subject: [PATCH 25/36] docs(help): regenerate the help file for the changed cmdlet pages Regenerate NTFSSecurity.dll-Help.xml from the pages with New-ExternalHelp and let platyPS wrap the syntax of the four add and remove pages for the parameters that are Object now. The pages and the file are a fixpoint: after a rebuild, Update-MarkdownHelp changes no page and New-ExternalHelp changes no file. State two sentences more plainly: what a blank value of the other parameter group does to the four add and remove cmdlets, and where Get-ChildItem2 -Filter differs from the built-in Get-ChildItem in PowerShell 7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Docs/Cmdlets/Add-NTFSAccess.md | 6 +- Docs/Cmdlets/Add-NTFSAudit.md | 6 +- Docs/Cmdlets/Get-ChildItem2.md | 2 +- Docs/Cmdlets/Remove-NTFSAccess.md | 10 +- Docs/Cmdlets/Remove-NTFSAudit.md | 10 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 368 ++++++++++--------- 6 files changed, 207 insertions(+), 195 deletions(-) diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 7829a40..07fcd41 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -22,8 +22,8 @@ Add-NTFSAccess [-Path] [-Account] [-AccessRigh ### PathComplex ``` Add-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] [-PropagationFlags ] [-PassThru] + [] ``` ### SDSimple @@ -46,7 +46,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DAC `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see [Concepts](../Concepts.md). -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index b59f886..0409530 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -22,8 +22,8 @@ Add-NTFSAudit [-Path] [-Account] [-AccessRight ### PathComplex ``` Add-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] - [-PassThru] [] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-PassThru] + [] ``` ### SDSimple @@ -46,7 +46,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. -A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 2db61e7..6964b49 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -148,7 +148,7 @@ For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. -PowerShell 7 deliberately differs here from the built-in `Get-ChildItem`: its own `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead, so these are deliberate differences from PowerShell 7's own `Get-ChildItem`. +In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index 20827da..c109569 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -22,15 +22,15 @@ Remove-NTFSAccess [-Path] [-Account] [-AccessR ### PathComplex ``` Remove-NTFSAccess [-Path] [-Account] [-AccessRights] - [-AccessType ] [-InheritanceFlags ] - [-PropagationFlags ] [-RemoveSpecific] [-PassThru] [] + [-AccessType ] [-InheritanceFlags ] [-PropagationFlags ] [-RemoveSpecific] [-PassThru] + [] ``` ### SDSimple ``` Remove-NTFSAccess [-SecurityDescriptor] [-Account] - [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] - [-PassThru] [] + [-AccessRights] [-AccessType ] -AppliesTo [-RemoveSpecific] [-PassThru] + [] ``` ### SDComplex @@ -48,7 +48,7 @@ Only the specified rights are taken away: when an entry grants more than `-Acces Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. ## EXAMPLES diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index 2921ce9..b8763f5 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -22,15 +22,15 @@ Remove-NTFSAudit [-Path] [-Account] [-AccessRi ### PathComplex ``` Remove-NTFSAudit [-Path] [-Account] [-AccessRights] - [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] - [-RemoveSpecific] [-PassThru] [] + [-AuditFlags ] [-InheritanceFlags ] [-PropagationFlags ] [-RemoveSpecific] [-PassThru] + [] ``` ### SDSimple ``` Remove-NTFSAudit [-SecurityDescriptor] [-Account] - [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] - [-PassThru] [] + [-AccessRights] [-AuditFlags ] -AppliesTo [-RemoveSpecific] [-PassThru] + [] ``` ### SDComplex @@ -46,7 +46,7 @@ The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access cont Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. -In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups, even if one of them is `$null` (for example a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`, is an `AmbiguousParameterSet` error: terminating when named directly on the command line, and reported for just that object, unchanged, when it comes from the pipeline. A literal `$null`, named or piped, for `-AppliesTo` itself fails the same way but with its own, unrelated error, because `-AppliesTo` is mandatory. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 93948b7..9ff82c5 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -12,7 +12,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DACL) of a file or a folder. Every account in `-Account` receives the rights in `-AccessRights`, either as an `Allow` or as a `Deny` entry. `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see Concepts (../Concepts.md). - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. `-Path` accepts pipeline input by value and by property name through its alias `FullName`, so output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` can be piped in. `-Account`, `-AccessRights`, `-AccessType`, `-InheritanceFlags`, and `-PropagationFlags` bind by property name as well, which lets you pipe `Security2.FileSystemAccessRule2` objects, or rows imported from a CSV file created from them, directly into the cmdlet. @@ -88,15 +88,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -104,7 +104,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -121,9 +121,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -212,15 +212,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -228,7 +228,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -245,9 +245,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -335,15 +335,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -351,7 +351,7 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). @@ -359,9 +359,9 @@ ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -380,16 +380,16 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -467,15 +467,15 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -483,7 +483,7 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). @@ -491,9 +491,9 @@ ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -512,16 +512,16 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -544,11 +544,11 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AccessControlType + Object - AccessControlType + Object Allow @@ -568,11 +568,11 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. - ApplyTo + Object - ApplyTo + Object None @@ -580,12 +580,12 @@ InheritanceFlags - Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. + Specifies which kind of child objects inherit the ACE. `ContainerInherit` passes the ACE on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the ACE on the item itself. The default is `ContainerInherit, ObjectInherit`. Inheritance flags have no effect on files, where the ACE is always created with `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. For details about the flags, see the .NET documentation of the InheritanceFlags Enum (https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.inheritanceflags). - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -617,11 +617,11 @@ PropagationFlags - Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. + Specifies how the ACE is propagated to child objects. `None` propagates the ACE to all levels that the inheritance flags allow, `InheritOnly` keeps the ACE from applying to the item it is defined on, and `NoPropagateInherit` limits inheritance to the direct children of the folder. The default is `None`, and propagation flags only have an effect in combination with `-InheritanceFlags`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -796,7 +796,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list (SACL) of a file or folder. Windows then writes an event to the security log when the audited account uses one of the audited access rights on the item. `-AuditFlags Success` audits successful attempts, `-AuditFlags Failure` audits failed attempts, and the default audits both. For what the individual access rights permit, see Concepts (../Concepts.md). In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. - A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. `-Path` accepts pipeline input by value and by property name through its `FullName` alias, so the output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` binds to it, and the remaining parameters bind by property name. The cmdlet writes no object unless you use `-PassThru`. @@ -872,7 +872,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -889,9 +889,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -899,16 +899,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -996,7 +996,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -1013,9 +1013,9 @@ SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -1023,16 +1023,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1120,16 +1120,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1137,16 +1137,16 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1165,16 +1165,16 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -1251,16 +1251,16 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1268,16 +1268,16 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1296,16 +1296,16 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -1340,11 +1340,11 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. - ApplyTo + Object - ApplyTo + Object None @@ -1352,11 +1352,11 @@ AuditFlags - Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. + Specifies which access attempts are audited. `Success` audits attempts that succeeded, `Failure` audits attempts that were denied, and `Success, Failure` audits both. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -1364,11 +1364,11 @@ InheritanceFlags - Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. + Specifies which child items inherit the audit entry. `ContainerInherit` passes the entry on to child folders, `ObjectInherit` passes it on to child files, and `None` keeps the entry on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. Use `-PropagationFlags` to control whether the entry also applies to the item itself and how far it propagates. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -1400,11 +1400,11 @@ PropagationFlags - Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. + Specifies how the inheritance selected with `-InheritanceFlags` propagates. `None` applies the entry to the item itself and to all inheriting child items, `InheritOnly` applies it to the inheriting child items but not to the item itself, and `NoPropagateInherit` limits inheritance to the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. The parameter has no effect when `-InheritanceFlags` is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -3497,7 +3497,7 @@ PS C:\> Disable-Privileges The `Get-ChildItem2` cmdlet lists the files and folders in the folders that you specify with `-Path`. It returns an `Alphaleonis.Win32.Filesystem.FileInfo` object for every file and an `Alphaleonis.Win32.Filesystem.DirectoryInfo` object for every folder. The cmdlet is the long-path counterpart of the built-in `Get-ChildItem` cmdlet: it enumerates the file system through the AlphaFS library (`Alphaleonis.Win32.Filesystem`) instead of `System.IO`, so it also returns items whose path is longer than the 260-character `MAX_PATH` limit. - If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name a folder, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. + If you omit `-Path`, the cmdlet lists the current location. Relative paths and the `.` and `..` notations are resolved against the current location. Each path must name an existing folder or file, and wildcard characters are not supported. The parameter accepts pipeline input by value and by the property name `FullName`, so you can pipe folders from `Get-ChildItem2` or `Get-Item2` into another `Get-ChildItem2` call, and you can pipe the result into `Get-NTFSAccess` and the other NTFSSecurity cmdlets. By default the cmdlet returns the immediate content of each folder and omits hidden items. Use `-Recurse` to walk the whole tree, `-Depth` to limit how deep the recursion goes, `-Filter` to restrict the result by name, `-Directory` or `-File` to restrict it by item type, and `-Force`, `-Hidden`, `-System`, `-ReadOnly`, or `-Attributes` to restrict it by file attributes. Two settings in the `PrivateData` section of the module manifest change the objects that this cmdlet emits. `GetFileSystemModeProperty` adds the `Mode` property, which shows the directory, archive, read-only, hidden, and system attributes in `darhs` notation. `IdentifyHardLinks` adds a `HardLinkCount` property to every file object. Both are `$true` by default and are read once when the cmdlet starts. A folder that the cmdlet cannot read produces a non-terminating error, and the enumeration continues with the next folder. If a folder cannot be opened while `-Recurse` looks for subfolders, the cmdlet reports the problem as a verbose message instead, so run the command with the `-Verbose` common parameter if you need to know which branches were skipped. @@ -3520,7 +3520,11 @@ PS C:\> Disable-Privileges Filter - Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. + Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + For example, `Report.*` also returns the item `Report`, which has no dot. + A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. + The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3724,7 +3728,11 @@ PS C:\> Disable-Privileges Filter - Specifies a name pattern that an item must match to be returned. The pattern supports the asterisk and the question mark as wildcard characters, an asterisk for any number of characters and a question mark for exactly one, and the match ignores case. Any other character stands for itself; a bracket is an ordinary character, so `Report[1].txt` returns the file of that name. As for `Get-ChildItem`, a pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name. The default value is `*`, which returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. + Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. + For example, `Report.*` also returns the item `Report`, which has no dot. + A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. + In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. + The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3867,7 +3875,9 @@ PS C:\> Disable-Privileges A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors. Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones. Earlier builds, including the 5.0.0 prereleases, could also omit the first hidden item with `-Hidden` unless `-Force` was explicitly supplied. Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. - The names are matched twice, by the enumeration of the AlphaFS library and by the cmdlet, which reads a dot as an ordinary character, and their rules for a dot can differ from those of `Get-ChildItem`: a pattern such as `Report.*` does not return the file `Report`, which has no dot, a pattern that ends in a dot returns nothing, and an empty value returns nothing. Only the pattern of an asterisk, a dot, and an asterisk is treated as a single asterisk. + The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. + For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. + PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these pattern shapes. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. @@ -5170,6 +5180,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd Before 5.0.0-rc7, the warning about a computer that couldn't be reached didn't name the computer, and the cmdlet warned for every name of this computer except `localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name. Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is denied" and no result on a computer in a domain, also for a name of this computer, such as the default `localhost`. Before 5.0.0-rc8, the cmdlet returned no access and no error when Windows refused to start the authorization manager for the computer of `-ServerName`, for example for a name of 32,718 characters or more, so that the result looked like an account without any rights. The cmdlet now writes a non-terminating `GetEffectiveAccessError` with the error that Windows reported and returns no result for the item, as it does for every other failure of the calculation. It warns that the result is based on this computer only when it calculated a result; before 5.0.0-rc8, the warning also came before an error when the computer couldn't be reached. + A user who has no domain credentials, such as a local standard user, still gets the error "Access is denied" from Windows when it asks for the effective access of a user account of the domain, also for a name of this computer; the cmdlet writes it as a non-terminating `GetEffectiveAccessError` and returns no result for that item. Accounts of this computer and groups such as `Everyone` or `Domain Users` give a result. @@ -5760,6 +5771,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message. If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. + A read failure that is not an access denial is reported the same way, as a non-terminating `ReadSecurityError`, categorized as `OpenError` and targeting the path; before 5.0.0-rc9, the cmdlet wrote a warning instead (`Could not read item {0}. The error was: {1}`) and returned no entry for the item, without an error to say why. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. @@ -7724,7 +7736,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Removes the rights in `-AccessRights` from the access control entries (ACEs) of a file or a folder. An entry is addressed by the account in `-Account`, the access type in `-AccessType`, and the inheritance and propagation flags, which are given either as `-AppliesTo` or as `-InheritanceFlags` and `-PropagationFlags`. Only the specified rights are taken away: when an entry grants more than `-AccessRights` names, the remaining rights stay in place, and the entry disappears only when all of its rights are removed. An `Allow` entry is always matched with the `Synchronize` right added to the specified rights. The flags must describe the entry as it exists on the item; when they do not, Windows splits the entry instead of removing the rights, so use the values that `Get-NTFSAccess` reports for the entry you want to change. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. @@ -7798,15 +7810,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -7814,7 +7826,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -7831,9 +7843,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -7933,15 +7945,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -7949,7 +7961,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -7966,9 +7978,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8067,15 +8079,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -8083,16 +8095,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8111,16 +8123,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -8209,15 +8221,15 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. Allow Deny - AccessControlType + Object - AccessControlType + Object Allow @@ -8225,16 +8237,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8253,16 +8265,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -8296,11 +8308,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AccessControlType + Object - AccessControlType + Object Allow @@ -8320,11 +8332,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. - ApplyTo + Object - ApplyTo + Object None @@ -8332,11 +8344,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. + Specifies the inheritance flags of the entry that is addressed. `ContainerInherit` marks an entry that child folders inherit, `ObjectInherit` marks an entry that child files inherit, and `None` marks an entry that is not inherited at all. The default is `ContainerInherit, ObjectInherit`, which is the scope `ThisFolderSubfoldersAndFiles`. Entries on files always carry `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8368,11 +8380,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. + Specifies the propagation flags of the entry that is addressed. `None` marks an entry that is inherited by all levels allowed by its inheritance flags, `InheritOnly` marks an entry that does not apply to the item it is defined on, and `NoPropagateInherit` marks an entry that is only inherited by the direct children of the folder. The default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -8563,7 +8575,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access control list (SACL) of a file or folder. The cmdlet builds an audit entry from `-Account`, `-AccessRights`, `-AuditFlags`, and the inheritance and propagation flags, and removes that entry from the SACL. The audit entries of the account are matched by their inheritance and propagation flags, and the requested access rights and audit flags are then taken away from them: an entry that audits further rights keeps those rights and disappears only when nothing is left. To remove an entry completely, pass the same values that `Get-NTFSAudit` reports for it. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. - In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both kinds, an object that has both `Path` and `SecurityDescriptor`, or one whose `AppliesTo` is `$null`, fails. `Import-Csv` gives `$null` for a column that a row lacks because it has fewer fields than the header. An `AppliesTo` that PowerShell can't convert to a scope, such as the blank cell of a CSV file or a misspelled name, is ignored, and the object is handled as one without `AppliesTo`: it uses its own `InheritanceFlags` and `PropagationFlags`, or the default scope if it has none. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -8638,7 +8650,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -8655,9 +8667,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8665,16 +8677,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8773,7 +8785,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. ThisFolderOnly @@ -8790,9 +8802,9 @@ PS C:\Data> Get-NTFSSecurityDescriptor SubfoldersOnlyOneLevel FilesOnlyOneLevel - ApplyTo + Object - ApplyTo + Object None @@ -8800,16 +8812,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8908,16 +8920,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -8925,16 +8937,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -8953,16 +8965,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -9050,16 +9062,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None Success Failure - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -9067,16 +9079,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None ContainerInherit ObjectInherit - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -9095,16 +9107,16 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. None NoPropagateInherit InheritOnly - PropagationFlags + Object - PropagationFlags + Object None @@ -9150,11 +9162,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. - ApplyTo + Object - ApplyTo + Object None @@ -9162,11 +9174,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor AuditFlags - Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. + Specifies which audited access attempts are removed from the entry. `Success` removes the auditing of successful attempts, `Failure` removes the auditing of denied attempts, and `Success, Failure` removes both. An entry that audits the flag you did not specify stays in place with that flag. The default is `Success, Failure`. A value that cannot be converted to one of these, including a blank one, is an `InvalidParameterValue` error: terminating when `-AuditFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - AuditFlags + Object - AuditFlags + Object Success, Failure @@ -9174,11 +9186,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor InheritanceFlags - Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. + Specifies the inheritance flags of the audit entry to remove. `ContainerInherit` addresses an entry that child folders inherit, `ObjectInherit` an entry that child files inherit, and `None` an entry that stays on the item itself. The values can be combined, and the default is `ContainerInherit, ObjectInherit`. The flags must match the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-InheritanceFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - InheritanceFlags + Object - InheritanceFlags + Object ContainerInherit, ObjectInherit @@ -9210,11 +9222,11 @@ PS C:\Data> Get-NTFSSecurityDescriptor PropagationFlags - Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. + Specifies the propagation flags of the audit entry to remove. `None` addresses an entry that applies to the item itself and to all inheriting child items, `InheritOnly` an entry that applies to the child items only, and `NoPropagateInherit` an entry whose inheritance stops at the direct children. The values `InheritOnly` and `NoPropagateInherit` can be combined, and the default is `None`. A value that cannot be converted to one of these flags, including a blank one, is an `InvalidParameterValue` error: terminating when `-PropagationFlags` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. - PropagationFlags + Object - PropagationFlags + Object None @@ -9949,7 +9961,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Set-NTFSSecurityDescriptor - + SecurityDescriptor Specifies one or more security descriptors that `Get-NTFSSecurityDescriptor` returned. Each descriptor is written to the file or folder it was read from, including every change that was made to it in memory. @@ -9987,7 +9999,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd False - + SecurityDescriptor Specifies one or more security descriptors that `Get-NTFSSecurityDescriptor` returned. Each descriptor is written to the file or folder it was read from, including every change that was made to it in memory. From accf6b8edd4df2e7471f168693e0190650e939b0 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 12:59:04 +0000 Subject: [PATCH 26/36] chore(release): prepare 5.0.0-rc9 Set the prerelease label to rc9 and add 5.0.0-rc8, which the PowerShell Gallery has had since 2026-10-20, to the versions that the Gallery has (Docs/Contributing/05-Releasing.md). A separate commit, so that it can be dropped if the changes of this branch go into another prerelease instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/NTFSSecurity.psd1 | 2 +- Tests/Repository.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NTFSSecurity/NTFSSecurity.psd1 b/NTFSSecurity/NTFSSecurity.psd1 index 4b32da8..0de98bc 100644 --- a/NTFSSecurity/NTFSSecurity.psd1 +++ b/NTFSSecurity/NTFSSecurity.psd1 @@ -102,7 +102,7 @@ ProjectUri = 'https://github.com/raandree/NTFSSecurity' ReleaseNotes = 'https://github.com/raandree/NTFSSecurity/blob/master/CHANGELOG.md' # Remove the prerelease label for the final release, see Docs/Contributing/05-Releasing.md - Prerelease = 'rc8' + Prerelease = 'rc9' } } } \ No newline at end of file diff --git a/Tests/Repository.Tests.ps1 b/Tests/Repository.Tests.ps1 index b0efc64..622c652 100644 --- a/Tests/Repository.Tests.ps1 +++ b/Tests/Repository.Tests.ps1 @@ -97,7 +97,7 @@ Describe 'Release metadata' { # The PowerShell Gallery doesn't accept a version twice. Add every published version to this list # (Docs/Contributing/05-Releasing.md). It 'Should not reuse a version that the PowerShell Gallery already has' { - $publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5', '5.0.0-rc6', '5.0.0-rc7' + $publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5', '5.0.0-rc6', '5.0.0-rc7', '5.0.0-rc8' $publishedVersions | Should -Not -Contain $version } From 1b8a3763a195d723b39ce31323309837395e8345 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 16:06:25 +0000 Subject: [PATCH 27/36] fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish -AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value, but PowerShell adds up the numbers of a comma list or an array for an enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a message. The conversion in ParameterConversion now rejects a string with a comma and a collection of more than one element for an enumeration that is not a flags enumeration, with the same InvalidParameterValue error as for a misspelled name (terminating for a named value, one error per object for a piped one). A single name and a collection of one element convert as before, and the flags parameters still take a list. The valid-value matrices of both published rc8 and this build give the same entries in both editions, named, piped by property name, and splatted, apart from the nine list rows. The completers of the five parameters compared with WildcardOptions.IgnoreCase only, so that a word typed in capitals did not complete while the culture of the thread was Turkish (an I and an i are different letters there, and THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now, like the filter of Get-ChildItem2. The tests pin both, red before the change and green after it in four configurations, and the headers of the two test files no longer say that the -SecurityDescriptor sets have no handler per descriptor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/EnumArgumentCompleters.cs | 2 +- NTFSSecurity/ParameterConversion.cs | 51 ++++++++++++++++++++++++-- Tests/ParameterSets.Access.Tests.ps1 | 47 ++++++++++++++++++++++-- Tests/ParameterSets.Audit.Tests.ps1 | 43 +++++++++++++++++++--- 4 files changed, 129 insertions(+), 14 deletions(-) diff --git a/NTFSSecurity/EnumArgumentCompleters.cs b/NTFSSecurity/EnumArgumentCompleters.cs index 927d1ea..5b9a280 100644 --- a/NTFSSecurity/EnumArgumentCompleters.cs +++ b/NTFSSecurity/EnumArgumentCompleters.cs @@ -38,7 +38,7 @@ namespace NTFSSecurity } } - var pattern = new WildcardPattern(word + "*", WildcardOptions.IgnoreCase); + var pattern = new WildcardPattern(word + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); var results = new List(); foreach (var name in Enum.GetNames(enumType)) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index 9adb60b..1ccfa94 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Management.Automation; namespace NTFSSecurity @@ -16,7 +17,9 @@ namespace NTFSSecurity /// /// Converts the raw value of a parameter to T, the way the PowerShell binder converts an argument for a /// parameter of that type: names in any case, a comma-separated list for a flags enumeration, a number, or - /// an enumeration instance. + /// an enumeration instance. An enumeration that is not a flags enumeration takes one value: PowerShell would + /// add up the numbers of a list for it, which selects a value that the caller did not name, so a list is + /// rejected for it. /// /// /// The raw value of the parameter. Not null: the caller keeps the default of the parameter for null instead @@ -25,9 +28,10 @@ namespace NTFSSecurity /// The name of the parameter, without the leading dash, for the message. /// The converted value. /// - /// The value is blank (an empty or a white-space-only string, the usual blank CSV cell) or the conversion - /// failed. The message names the value, unless it is blank, the parameter, and the valid names of T; the - /// inner exception of a failed conversion is the error that LanguagePrimitives.ConvertTo raised. + /// The value is blank (an empty or a white-space-only string, the usual blank CSV cell), is a list for an + /// enumeration that is not a flags enumeration, or the conversion failed. The message names the value, unless + /// it is blank, the parameter, and the valid names of T; the inner exception of a failed conversion is the + /// error that LanguagePrimitives.ConvertTo raised. /// internal static T Convert(object value, string parameterName) { @@ -38,6 +42,17 @@ namespace NTFSSecurity "The value for parameter -{0} is blank. {1}", parameterName, DescribeValidValues(typeof(T)))); } + if (!typeof(T).IsDefined(typeof(FlagsAttribute), false)) + { + var list = DescribeList(value); + if (list != null) + { + throw new ArgumentException(string.Format( + "The value '{0}' for parameter -{1} is not valid: the parameter takes one value, not a list. {2}", + list, parameterName, DescribeValidValues(typeof(T)))); + } + } + try { return (T)LanguagePrimitives.ConvertTo(value, typeof(T)); @@ -63,6 +78,34 @@ namespace NTFSSecurity return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); } + // The text of a list that the conversion would add up, for the message: a string that holds a comma, or a + // collection of more than one element (a wrapped PSObject counts as what it wraps). Null for any other value, + // such as a single name or a collection of one element, which convert to that one value. + private static string DescribeList(object value) + { + var unwrapped = value is PSObject ? ((PSObject)value).BaseObject : value; + + var text = unwrapped as string; + if (text != null) + { + return text.IndexOf(',') >= 0 ? text : null; + } + + var list = unwrapped as IList; + if (list == null || list.Count < 2) + { + return null; + } + + var items = new string[list.Count]; + for (var i = 0; i < items.Length; i++) + { + items[i] = string.Format("{0}", list[i]); + } + + return string.Join(", ", items); + } + // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a // combination of them is written separated with commas. private static string DescribeValidValues(Type enumType) diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 75481c0..e8d6679 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -4,8 +4,7 @@ found untested, with the module built in NTFSSecurity\bin\Release on files in a sandbox folder. Each test closes a gap the matrix recorded for the row it names (state, error, pipeline, continuation, or output); it does not repeat what Access.Tests.ps1, PathErrors.Tests.ps1, PermissionScopes.Tests.ps1, SecurityDescriptorSets.Tests.ps1, or - PipelineControl.Tests.ps1 already assert. The -SecurityDescriptor sets of Add-NTFSAccess and Remove-NTFSAccess have - no per-item error handler (an open item for the maintainer); no test here makes one of those two sets fail. + PipelineControl.Tests.ps1 already assert. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' @@ -1351,9 +1350,18 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'a comma list of two names'; Value = 'Allow,Deny'; ValueText = 'Allow,Deny' } + @{ Shape = 'an array of two names'; Value = @('Allow', 'Deny'); ValueText = 'Allow, Deny' } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5) = 40 rows. + # AccessType and AppliesTo name one value each, not a combination: PowerShell would add the numbers of a list, and + # an entry for a scope that the caller did not name would follow. + $accessAppliesToListShapes = @( + @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + ) + + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (7 + 3 * 5 + 2) = 48 rows. $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { foreach ($shapeCase in $accessTypeBadShapes) { @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } @@ -1363,6 +1371,9 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } } } + foreach ($shapeCase in $accessAppliesToListShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } } $accessSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { @@ -1382,6 +1393,10 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Allow,Deny' } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @('Allow', 'Deny') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } } # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must @@ -1399,6 +1414,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Description = 'a different case'; Value = 'deny' } @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AccessControlType]::Deny } @{ Description = 'an integer'; Value = 1 } + @{ Description = 'an array of one name'; Value = @('Deny') } ) $accessValidFlagsShapeCases = @( @@ -1432,6 +1448,14 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Parameter = 'AppliesTo'; Word = 'ThisFolderO'; ExpectedName = 'ThisFolderOnly' } ) + # In Turkish, a case-insensitive comparison of the culture of the thread treats I and i as different letters, so + # a word typed in capitals completes only when the completers compare culture invariant. + $accessTurkishCompletionCases = @( + @{ Parameter = 'AppliesTo'; Word = 'THISFOLDERO'; ExpectedName = 'ThisFolderOnly' } + @{ Parameter = 'InheritanceFlags'; Word = 'CONTAINERINH'; ExpectedName = 'ContainerInherit' } + @{ Parameter = 'PropagationFlags'; Word = 'NOPROPAGATEINHERI'; ExpectedName = 'NoPropagateInherit' } + ) + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers @@ -1879,6 +1903,23 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName } + + It 'Should complete Add-NTFSAccess - for the word while the culture of the thread is Turkish' -ForEach $accessTurkishCompletionCases { + $inputScript = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} {1}' -f $Parameter, $Word + $original = [System.Threading.Thread]::CurrentThread.CurrentCulture + try { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR') + 'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything' + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + $names = @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) + } + finally { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $original + } + + $names | Should -Contain $ExpectedName + } } } diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index d892875..a1a67ae 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -3,11 +3,7 @@ the audit cmdlets: pipeline binding by value and by property name, continuation past a failing path or descriptor, and state or output assertions beyond -PassThru, with the module built in NTFSSecurity\bin\Release on files in a sandbox folder. Reading and changing audit entries needs the Security privilege; tests that need - it skip without it and run in CI, whose runners are elevated, like Audit.Tests.ps1. The -SecurityDescriptor sets - of Add-NTFSAudit and Remove-NTFSAudit have no per-item handler around the change itself (an open maintainer - item), so no test here pins what happens when that change throws for one of several descriptors; the only - failure the -SecurityDescriptor scenarios below exercise is the already-handled one of a descriptor that was - read without its audit section. + it skip without it and run in CI, whose runners are elevated, like Audit.Tests.ps1. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' @@ -1023,7 +1019,14 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5) = 40 rows. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5 + 2) = 42 rows. + # AppliesTo names one value, not a combination: PowerShell would add the numbers of a list, and an entry for a + # scope that the caller did not name would follow. AuditFlags and the two flags parameters take a list. + $auditAppliesToListShapes = @( + @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + ) + $auditInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { foreach ($shapeCase in $auditFlagsBadShapes) { @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } @@ -1033,6 +1036,9 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = $parameterName; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } } } + foreach ($shapeCase in $auditAppliesToListShapes) { + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } + } } $auditSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { @@ -1052,6 +1058,8 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'PropagationFlags'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } } # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a @@ -1100,6 +1108,12 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Parameter = 'AuditFlags'; Word = 'F'; ExpectedName = 'Failure' } ) + # In Turkish, a case-insensitive comparison of the culture of the thread treats I and i as different letters, so + # a word typed in capitals completes only when the completers compare culture invariant. + $auditTurkishCompletionCases = @( + @{ Parameter = 'AuditFlags'; Word = 'FAIL'; ExpectedName = 'Failure' } + ) + # Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a # property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple # group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers @@ -1549,5 +1563,22 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF $r4 = [System.Management.Automation.CommandCompletion]::CompleteInput($wildcard, $wildcard.Length, $null) @($r4.CompletionMatches.CompletionText) | Should -Contain $ExpectedName } + + It 'Should complete Add-NTFSAudit - for the word while the culture of the thread is Turkish' -ForEach $auditTurkishCompletionCases { + $inputScript = 'Add-NTFSAudit -Path C:\x -Account a -AccessRights Read -{0} {1}' -f $Parameter, $Word + $original = [System.Threading.Thread]::CurrentThread.CurrentCulture + try { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR') + 'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything' + + $completion = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $inputScript.Length, $null) + $names = @($completion.CompletionMatches | ForEach-Object -Process { $_.CompletionText }) + } + finally { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $original + } + + $names | Should -Contain $ExpectedName + } } } From 42f07a55f0a1f92383aaa5b8fc9e618d24979856 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 16:06:25 +0000 Subject: [PATCH 28/36] docs: correct the statements that the second review found and regenerate the help file Say in the pages and in the CHANGELOG what a script block, a named $null of -AppliesTo, a list for -AccessType or -AppliesTo, and a piped value that PowerShell cannot convert did before 5.0.0 and do now; name the carry-over of the scope between piped descriptors for the objects that it affected; mark the descriptor sets that go on after a failing descriptor and the checks of the object with both parameter groups as breaking, with the hint that a script that relied on the stop needs -ErrorAction Stop. Describe where Get-ChildItem2 -Filter differs from the built-in filter of PowerShell 7 with the shapes that really differ (a pattern that ends in a dot, an asterisk followed by a dot and question marks) and state the two cases that are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, and a pattern that ends in two dots is accepted. The pages and the help file are a fixpoint again after a rebuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 37 +++++++----- Docs/Cmdlets/Add-NTFSAccess.md | 6 +- Docs/Cmdlets/Add-NTFSAudit.md | 4 +- Docs/Cmdlets/Get-ChildItem2.md | 6 +- Docs/Cmdlets/Remove-NTFSAccess.md | 6 +- Docs/Cmdlets/Remove-NTFSAudit.md | 4 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 60 ++++++++++---------- 7 files changed, 69 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2480980..c8c7719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,10 +105,14 @@ The format is based on `*.*.*` returns every item; and a question mark also matches nothing at the end of a name and before a dot, so `a?` also returns `a`. An empty `-Filter` returns every item, like `*`, instead of nothing, and a pattern - of more than 32,767 characters is a parameter error. In PowerShell 7, - `Get-ChildItem2` deliberately differs from its own `Get-ChildItem -Filter` - for a pattern that ends in a dot, unless an asterisk comes right before the - dot, and for a pattern of an asterisk, a dot, and one or two question marks + of more than 32,767 characters is a parameter error. Two edge cases are not + like Windows PowerShell 5.1: a blank at the end of a pattern is part of it, + where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots + is accepted, where Windows PowerShell 5.1 stops with an error. In + PowerShell 7, `Get-ChildItem2` deliberately differs from + `Get-ChildItem -Filter` for some patterns, for example for a pattern that + ends in a dot, unless an asterisk comes right before the dot, and for a + pattern in which an asterisk is followed by a dot and question marks - **Breaking:** write a non-terminating error in `Get-NTFSOrphanedAccess` for an item that it can't read for a reason other than a denied access, and continue with the next item: `ReadSecurityError` with the category @@ -303,11 +307,12 @@ The format is based on `-PropagationFlags`; `-AppliesTo` is now mandatory in the `Simple` parameter sets, so such a command uses the flag parameters and their defaults, as for a path -- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and +- **Breaking:** fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and `Remove-NTFSAudit`, which ignored the `AppliesTo` property of an object piped to them with a path and used the default scope, and which gave a - piped security descriptor object without a scope the scope of the object - before it; the property now sets the scope of its own object only. A piped + piped security descriptor object that had only `InheritanceFlags` or + `PropagationFlags` the scope of the object before it; the property now sets + the scope of its own object only. A piped object that has `AppliesTo` and `InheritanceFlags` or `PropagationFlags` now fails with `AmbiguousParameterSet`, like the same parameters on the command line, and so does an object that has both `Path` and @@ -335,16 +340,22 @@ The format is based on have the type `Object`, because the cmdlets convert the values themselves. A bad value that you name on the command line, and a named `$null`, still stop the command, but with the error id `InvalidParameterValue`, not the id - of a parameter binding error. A script block, such as - `-AccessType { 'Deny' }`, is no longer evaluated for every piped object and - is a bad value. `Get-Help` shows the type `Object`, and tab completion - still offers the valid names -- Fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and + of a parameter binding error; `-AppliesTo $null` keeps the error of the + binder, because the parameter is mandatory. `-AccessType` and `-AppliesTo` + take one value: a comma list or an array of several values is an + `InvalidParameterValue` error, where PowerShell added up the numbers of the + values, so that `-AppliesTo 'ThisFolderOnly,FilesOnly'` created a + `FilesOnly` entry and `-AccessType 'Allow,Deny'` a `Deny` entry. A script + block, such as `-AccessType { 'Deny' }`, is no longer evaluated for every + piped object and is a bad value. `Get-Help` shows the type `Object`, and + tab completion still offers the valid names +- **Breaking:** fix `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and `Remove-NTFSAudit` with `-SecurityDescriptor`, which ended the whole command with a terminating error when the change of one descriptor failed, such as for a deny entry without rights; they write the non-terminating error `AddAceError` or `RemoveAceError` for that descriptor and go on with - the next one, as they do for a path + the next one, as they do for a path. A script that relies on the stop needs + `-ErrorAction Stop` - Fix `Get-NTFSEffectiveAccess`: `-ExcludeNoneAccessEntries` now leaves out items without access, the cmdlet uses the current location when `-Path` is omitted, and `-SecurityDescriptor` returns the effective access of the diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 07fcd41..9ff8ee5 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -46,7 +46,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DAC `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see [Concepts](../Concepts.md). -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. @@ -107,7 +107,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. +Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. ```yaml Type: Object @@ -140,7 +140,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. +Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ```yaml Type: Object diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index 0409530..d85ab86 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -46,7 +46,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. -A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. @@ -125,7 +125,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. +Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ```yaml Type: Object diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 6964b49..5c11323 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -148,7 +148,9 @@ For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. -In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. +`Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + +Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. @@ -331,7 +333,7 @@ The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" w For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. -PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these pattern shapes. +PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index c109569..4520c73 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -48,7 +48,7 @@ Only the specified rights are taken away: when an entry grants more than `-Acces Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. -The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. +The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. ## EXAMPLES @@ -105,7 +105,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. +Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. ```yaml Type: Object @@ -138,7 +138,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. +Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ```yaml Type: Object diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index b8763f5..878ce10 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -46,7 +46,7 @@ The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access cont Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. -In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. +In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -123,7 +123,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. +Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ```yaml Type: Object diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 9ff82c5..3ffd866 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -12,7 +12,7 @@ Adds an access control entry (ACE) to the discretionary access control list (DACL) of a file or a folder. Every account in `-Account` receives the rights in `-AccessRights`, either as an `Allow` or as a `Deny` entry. `-AccessRights` accepts the basic rights such as `Read`, `Modify`, and `FullControl` as well as the granular rights such as `CreateFiles` or `WriteAttributes`, and several values can be combined, for example `-AccessRights ReadData, WriteData, Delete`. For the mapping between the values of this module, the rights that Windows displays, and the entries of the advanced security dialog, see Concepts (../Concepts.md). - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`; both describe the same ACE flags. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When `-AccessType`, `-AppliesTo`, `-InheritanceFlags`, and `-PropagationFlags` are omitted, the cmdlet adds an `Allow` ACE that applies to this folder, subfolders, and files, which corresponds to the inheritance flags `ContainerInherit, ObjectInherit` and no propagation flags. An `Allow` ACE always receives the `Synchronize` right in addition to the requested rights, inheritance and propagation flags are ignored on files, and rights for an account that already has an ACE with the same access type and the same flags are merged into that ACE. The cmdlet writes no output unless `-PassThru` is used, and a failure on one item is reported as a non-terminating error while the remaining items are processed. `-Path` accepts pipeline input by value and by property name through its alias `FullName`, so output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` can be piped in. `-Account`, `-AccessRights`, `-AccessType`, `-InheritanceFlags`, and `-PropagationFlags` bind by property name as well, which lets you pipe `Security2.FileSystemAccessRule2` objects, or rows imported from a CSV file created from them, directly into the cmdlet. @@ -88,7 +88,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -104,7 +104,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -212,7 +212,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -228,7 +228,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -335,7 +335,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -467,7 +467,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -544,7 +544,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Object @@ -568,7 +568,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. Object @@ -796,7 +796,7 @@ The `Add-NTFSAudit` cmdlet adds an audit entry to the system access control list (SACL) of a file or folder. Windows then writes an event to the security log when the audited account uses one of the audited access rights on the item. `-AuditFlags Success` audits successful attempts, `-AuditFlags Failure` audits failed attempts, and the default audits both. For what the individual access rights permit, see Concepts (../Concepts.md). In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path`, adds the entry, and writes the descriptor back right away. In the `SDSimple` and `SDComplex` parameter sets it adds the entry to an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned; that change only reaches the file system when you pass the object to `Set-NTFSSecurityDescriptor`. The simple sets describe the scope of the entry with the single `-AppliesTo` parameter, the complex sets with `-InheritanceFlags` and `-PropagationFlags`. - A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`, and `-AppliesTo` is `ThisFolderSubfoldersAndFiles`, so both the simple and the complex set audit the item, its subfolders, and its files by default. Inheritance applies to folders only: when the item is a file, the cmdlet stores the entry without inheritance and propagation flags. `-Path` accepts pipeline input by value and by property name through its `FullName` alias, so the output of `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` binds to it, and the remaining parameters bind by property name. The cmdlet writes no object unless you use `-PassThru`. @@ -872,7 +872,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -996,7 +996,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -1340,7 +1340,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. Object @@ -3523,7 +3523,8 @@ PS C:\> Disable-Privileges Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. - In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3731,7 +3732,8 @@ PS C:\> Disable-Privileges Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. - In PowerShell 7, `Get-ChildItem2` deliberately differs from the built-in `Get-ChildItem` for two pattern shapes: the built-in `-Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3877,7 +3879,7 @@ PS C:\> Disable-Privileges Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. - PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it returns every item for a pattern of an asterisk, a dot, and one or two question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these pattern shapes. + PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. @@ -7736,7 +7738,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Removes the rights in `-AccessRights` from the access control entries (ACEs) of a file or a folder. An entry is addressed by the account in `-Account`, the access type in `-AccessType`, and the inheritance and propagation flags, which are given either as `-AppliesTo` or as `-InheritanceFlags` and `-PropagationFlags`. Only the specified rights are taken away: when an entry grants more than `-AccessRights` names, the remaining rights stay in place, and the entry disappears only when all of its rights are removed. An `Allow` entry is always matched with the `Synchronize` right added to the specified rights. The flags must describe the entry as it exists on the item; when they do not, Windows splits the entry instead of removing the rights, so use the values that `Get-NTFSAccess` reports for the entry you want to change. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Inherited entries cannot be removed from the item that inherits them. Remove them from the folder named in the `InheritedFrom` property, or run `Disable-NTFSAccessInheritance` on the item first, which copies the inherited entries into it as explicit ones that this cmdlet can then remove. - The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. + The cmdlet has four parameter sets. The `Path` sets read the item from disk and write the changed DACL back immediately, while the `SD` sets change a `Security2.FileSystemSecurity2` object returned by `Get-NTFSSecurityDescriptor` in memory until `Set-NTFSSecurityDescriptor` writes it back. The `Simple` sets take `-AppliesTo`, the `Complex` sets take `-InheritanceFlags` and `-PropagationFlags`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AccessType`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. All relevant parameters bind by property name, so the output of `Get-NTFSAccess` and `Get-NTFSOrphanedAccess` can be piped directly into this cmdlet. The cmdlet writes no output unless `-PassThru` is used. @@ -7810,7 +7812,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -7826,7 +7828,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -7945,7 +7947,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -7961,7 +7963,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -8079,7 +8081,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -8221,7 +8223,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Allow @@ -8308,7 +8310,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. Object @@ -8332,7 +8334,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. Object @@ -8575,7 +8577,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor The `Remove-NTFSAudit` cmdlet removes an audit entry from the system access control list (SACL) of a file or folder. The cmdlet builds an audit entry from `-Account`, `-AccessRights`, `-AuditFlags`, and the inheritance and propagation flags, and removes that entry from the SACL. The audit entries of the account are matched by their inheritance and propagation flags, and the requested access rights and audit flags are then taken away from them: an entry that audits further rights keeps those rights and disappears only when nothing is left. To remove an entry completely, pass the same values that `Get-NTFSAudit` reports for it. With `-RemoveSpecific`, the cmdlet removes only an entry that matches exactly. Because the inheritance and propagation flags take part in the match, they must describe the entry you want to remove. `-AppliesTo ThisFolderOnly` removes an entry that is not inherited by child items, which is also the shape of every audit entry on a file, while the default of the complex parameter sets removes an entry that applies to the folder, its subfolders, and its files. An entry that an item inherits from a parent folder is stored on that parent, so remove it there, or use `Clear-NTFSAudit` with `-DisableInheritance` to drop the inherited entries on the item. - In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. A script block is a plain value for these parameters, not evaluated once for each piped object the way it would be for a strongly typed parameter, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type. Before 5.0.0, such a value was silently ignored, and the object was handled as one without it. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. + In the `PathSimple` and `PathComplex` parameter sets the cmdlet reads the security descriptor of every item in `-Path` and writes it back right away. In the `SDSimple` and `SDComplex` parameter sets it changes an in-memory `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, and the change reaches the file system only when you pass the object to `Set-NTFSSecurityDescriptor`. A command without `-AppliesTo` uses a `Complex` set, also when it works on a security descriptor, and a command that has `-AppliesTo` together with `-InheritanceFlags` or `-PropagationFlags` fails, because PowerShell can't choose between the sets. The same holds for an object that you pipe in: its property `AppliesTo` selects a `Simple` set, its properties `InheritanceFlags` and `PropagationFlags` select a `Complex` set, and an object that has both `Path` and `SecurityDescriptor` fails. A `$null`, an empty string, or `DBNull` value of a property counts as present for this choice, the same as a real value, so a CSV file, or any other source of piped objects, should have the columns of only one group: `AppliesTo`, or `InheritanceFlags` and `PropagationFlags`. A row or object that has properties of both groups is an `AmbiguousParameterSet` error, even if the value of one of them is `$null` (for example in a short CSV row, whose missing trailing columns `Import-Csv` fills with `$null`), an empty string, or `DBNull`: the error is terminating when you name the parameters on the command line, and it is reported for just that object, which is left unchanged, when the object comes from the pipeline. A `$null` value of `-AppliesTo` itself, named or piped, fails too, but with the binder's own error for a mandatory parameter, `ParameterArgumentValidationErrorNullNotAllowed`. Within the group that PowerShell did choose, an `AppliesTo`, `AuditFlags`, `InheritanceFlags`, or `PropagationFlags` value that PowerShell can't convert to its type, such as a blank cell or a misspelled name, is an `InvalidParameterValue` error for that object: the object is left unchanged, and the next one in the pipeline is still processed; the same bad value named on the command line fails the whole command instead. Before 5.0.0, such a piped value was silently ignored, and the object was handled as one without it. A script block is a plain value for these parameters, so a named script block, for example `-AppliesTo { 'FilesOnly' }`, fails with a terminating `InvalidParameterValue` error, the same as any other named value of the wrong type; before 5.0.0, a script block was evaluated once for each piped object. Before 5.0.0, a command that used `-SecurityDescriptor` without `-AppliesTo`, `-InheritanceFlags`, or `-PropagationFlags` failed, because PowerShell couldn't choose between the two `SD` sets. When you omit them, `-AuditFlags` is `Success, Failure`, `-InheritanceFlags` is `ContainerInherit, ObjectInherit`, `-PropagationFlags` is `None`. All parameters bind by property name, and `-Path` also binds by value and through its `FullName` alias, so you can pipe the output of `Get-NTFSAudit`, `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` into the cmdlet. The cmdlet writes no object unless you use `-PassThru`. @@ -8650,7 +8652,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -8785,7 +8787,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. ThisFolderOnly @@ -9162,7 +9164,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. Object From e39aeed1b24aa9cc3c40070dac5dc3f464c0c59e Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 16:28:52 +0000 Subject: [PATCH 29/36] fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo The rejection of a list for the enumerations that are not flags enumerations looked only at a string with a comma and at an IList of more than one element. A collection that is not an IList (a HashSet, a Queue, the keys of a hashtable, a LINQ sequence), a collection of one element that holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and a collection that holds a collection of several elements still reached LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo $scopes.Keys created a ThisFolderAndFiles entry that nobody named. ParameterConversion takes the one value out of the argument now: a wrapped PSObject counts as what it wraps, a collection of one element converts as its element (also when that element is a collection of one element), and a collection of several elements or an element with a comma is the list that is rejected. A dictionary, an empty collection, and a collection that cannot be enumerated keep going to the conversion, which reports them as before. A blank value inside a collection of one element is reported as blank. The tests add the shapes to the bad-value tables of the four cmdlets (32 rows, red before the change and green after it in four configurations) and pin that a list of one name and a hash set of one name stay valid. The valid-value matrices of rc8 and this build still differ in the nine list rows only, and a probe of 50 shapes shows every collection of several values rejected and every single value accepted, in both editions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 97 +++++++++++++++++++--------- Tests/ParameterSets.Access.Tests.ps1 | 20 +++++- Tests/ParameterSets.Audit.Tests.ps1 | 12 +++- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index 1ccfa94..738bf21 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -18,8 +18,9 @@ namespace NTFSSecurity /// Converts the raw value of a parameter to T, the way the PowerShell binder converts an argument for a /// parameter of that type: names in any case, a comma-separated list for a flags enumeration, a number, or /// an enumeration instance. An enumeration that is not a flags enumeration takes one value: PowerShell would - /// add up the numbers of a list for it, which selects a value that the caller did not name, so a list is - /// rejected for it. + /// add up the numbers of a list for it, so that one of the values wins silently or a value that the caller + /// did not name is chosen, so a list is rejected for it, in whatever kind of collection it comes. A + /// collection of one element converts as that element. /// /// /// The raw value of the parameter. Not null: the caller keeps the default of the parameter for null instead @@ -35,16 +36,12 @@ namespace NTFSSecurity /// internal static T Convert(object value, string parameterName) { - var text = value as string; - if (text != null && string.IsNullOrWhiteSpace(text)) - { - throw new ArgumentException(string.Format( - "The value for parameter -{0} is blank. {1}", parameterName, DescribeValidValues(typeof(T)))); - } + var convertible = value; if (!typeof(T).IsDefined(typeof(FlagsAttribute), false)) { - var list = DescribeList(value); + string list; + convertible = TakeOneValue(value, out list); if (list != null) { throw new ArgumentException(string.Format( @@ -53,9 +50,16 @@ namespace NTFSSecurity } } + var text = convertible as string; + if (text != null && string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException(string.Format( + "The value for parameter -{0} is blank. {1}", parameterName, DescribeValidValues(typeof(T)))); + } + try { - return (T)LanguagePrimitives.ConvertTo(value, typeof(T)); + return (T)LanguagePrimitives.ConvertTo(convertible, typeof(T)); } catch (Exception ex) { @@ -78,32 +82,65 @@ namespace NTFSSecurity return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); } - // The text of a list that the conversion would add up, for the message: a string that holds a comma, or a - // collection of more than one element (a wrapped PSObject counts as what it wraps). Null for any other value, - // such as a single name or a collection of one element, which convert to that one value. - private static string DescribeList(object value) + // The one value that the conversion takes from the argument of an enumeration that is not a flags enumeration: + // the argument itself, or the only element of a collection of any kind (a wrapped PSObject counts as what it + // wraps, and a collection that holds a collection is opened too). When the argument holds more than one + // value, list is its text for the message: a string that holds a comma, a collection of several elements, or + // a collection of one element that holds such a list; the returned value is then not used. A dictionary, an + // empty collection, or a collection that cannot be enumerated is returned as it is, for the conversion to + // reject or convert with its own error. + private static object TakeOneValue(object value, out string list) { - var unwrapped = value is PSObject ? ((PSObject)value).BaseObject : value; + list = null; + var current = value; - var text = unwrapped as string; - if (text != null) + while (true) { - return text.IndexOf(',') >= 0 ? text : null; - } + var unwrapped = current is PSObject ? ((PSObject)current).BaseObject : current; - var list = unwrapped as IList; - if (list == null || list.Count < 2) - { - return null; - } + var text = unwrapped as string; + if (text != null) + { + if (text.IndexOf(',') >= 0) + { + list = text; + } - var items = new string[list.Count]; - for (var i = 0; i < items.Length; i++) - { - items[i] = string.Format("{0}", list[i]); - } + return unwrapped; + } + + var collection = unwrapped as IEnumerable; + if (collection == null || unwrapped is IDictionary) + { + return unwrapped; + } + + var items = new System.Collections.Generic.List(); + try + { + foreach (var item in collection) + { + items.Add(item); + } + } + catch (Exception) + { + return value; + } + + if (items.Count == 1) + { + current = items[0]; + continue; + } - return string.Join(", ", items); + if (items.Count > 1) + { + list = string.Join(", ", items.ConvertAll(item => string.Format("{0}", item))); + } + + return value; + } } // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index e8d6679..6bdbb64 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1352,16 +1352,24 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } @{ Shape = 'a comma list of two names'; Value = 'Allow,Deny'; ValueText = 'Allow,Deny' } @{ Shape = 'an array of two names'; Value = @('Allow', 'Deny'); ValueText = 'Allow, Deny' } + @{ Shape = 'an array of one comma list'; Value = @('Allow,Deny'); ValueText = 'Allow,Deny' } + @{ Shape = 'an array that holds an array of two names'; Value = , @('Allow', 'Deny'); ValueText = 'Allow, Deny' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Allow', 'Deny')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ Allow = 1; Deny = 2 }).Keys; ValueText = $null } ) - # AccessType and AppliesTo name one value each, not a combination: PowerShell would add the numbers of a list, and - # an entry for a scope that the caller did not name would follow. + # AccessType and AppliesTo name one value each, not a combination: PowerShell would add the numbers of a list, so that + # one of the values wins silently or, for AppliesTo, a scope that the caller did not name is chosen. Every kind of + # collection counts, also a collection of one element that holds a list. $accessAppliesToListShapes = @( @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (7 + 3 * 5 + 2) = 48 rows. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (11 + 3 * 5 + 5) = 62 rows. $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { foreach ($shapeCase in $accessTypeBadShapes) { @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } @@ -1395,8 +1403,12 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Allow,Deny' } @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @('Allow', 'Deny') } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @('Allow,Deny') } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Allow', 'Deny')) } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } } # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must @@ -1415,6 +1427,8 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Description = 'the enum value'; Value = [System.Security.AccessControl.AccessControlType]::Deny } @{ Description = 'an integer'; Value = 1 } @{ Description = 'an array of one name'; Value = @('Deny') } + @{ Description = 'a list of one name'; Value = [System.Collections.Generic.List[string]]::new([string[]]@('Deny')) } + @{ Description = 'a hash set of one name'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Deny')) } ) $accessValidFlagsShapeCases = @( diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index a1a67ae..6361a2a 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1019,12 +1019,16 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5 + 2) = 42 rows. - # AppliesTo names one value, not a combination: PowerShell would add the numbers of a list, and an entry for a - # scope that the caller did not name would follow. AuditFlags and the two flags parameters take a list. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5 + 5) = 50 rows. + # AppliesTo names one value, not a combination: PowerShell would add the numbers of a list, so that one of the + # values wins silently or a scope that the caller did not name is chosen. AuditFlags and the two flags parameters + # take a list. Every kind of collection counts, also a collection of one element that holds a list. $auditAppliesToListShapes = @( @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } + @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } + @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } + @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } ) $auditInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { @@ -1060,6 +1064,8 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } } # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a From e3eed8c773ce23ec98b767bf00a2b27e70c79858 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 16:28:52 +0000 Subject: [PATCH 30/36] docs: state the reason for the list rule and the edge cases of the filter exactly Say that a list for -AccessType or -AppliesTo lets one of the values win silently or, for -AppliesTo, selects a scope that nobody named (the sum of two Allow and Deny numbers is always one of the two), and that every kind of collection counts. State the third edge case of Get-ChildItem2 -Filter (blanks at the end of the pattern, also before a final dot), limit the error of Windows PowerShell 5.1 to an even number of dots at the end, and name the pattern of a single dot, for which the built-in filter of PowerShell 7 returns every item. The help file is regenerated, and the pages and the file are a fixpoint after a rebuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 16 +++--- Docs/Cmdlets/Add-NTFSAccess.md | 4 +- Docs/Cmdlets/Add-NTFSAudit.md | 2 +- Docs/Cmdlets/Get-ChildItem2.md | 6 +-- Docs/Cmdlets/Remove-NTFSAccess.md | 4 +- Docs/Cmdlets/Remove-NTFSAudit.md | 2 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 54 ++++++++++---------- 7 files changed, 45 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c7719..6a04b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,14 +105,16 @@ The format is based on `*.*.*` returns every item; and a question mark also matches nothing at the end of a name and before a dot, so `a?` also returns `a`. An empty `-Filter` returns every item, like `*`, instead of nothing, and a pattern - of more than 32,767 characters is a parameter error. Two edge cases are not - like Windows PowerShell 5.1: a blank at the end of a pattern is part of it, - where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots - is accepted, where Windows PowerShell 5.1 stops with an error. In + of more than 32,767 characters is a parameter error. Two kinds of pattern + are not like Windows PowerShell 5.1: blanks at the end of a pattern, also + before a final dot, are part of it, where Windows PowerShell 5.1 drops them, + and a pattern that ends in an even number of dots, such as two, is + accepted, where Windows PowerShell 5.1 stops with an error. In PowerShell 7, `Get-ChildItem2` deliberately differs from `Get-ChildItem -Filter` for some patterns, for example for a pattern that - ends in a dot, unless an asterisk comes right before the dot, and for a - pattern in which an asterisk is followed by a dot and question marks + ends in a dot, unless an asterisk comes right before the dot (the pattern + of a single dot returns every item), and for a pattern in which an asterisk + is followed by a dot and question marks - **Breaking:** write a non-terminating error in `Get-NTFSOrphanedAccess` for an item that it can't read for a reason other than a denied access, and continue with the next item: `ReadSecurityError` with the category @@ -342,7 +344,7 @@ The format is based on stop the command, but with the error id `InvalidParameterValue`, not the id of a parameter binding error; `-AppliesTo $null` keeps the error of the binder, because the parameter is mandatory. `-AccessType` and `-AppliesTo` - take one value: a comma list or an array of several values is an + take one value: a comma list or a collection of several values is an `InvalidParameterValue` error, where PowerShell added up the numbers of the values, so that `-AppliesTo 'ThisFolderOnly,FilesOnly'` created a `FilesOnly` entry and `-AccessType 'Allow,Deny'` a `Deny` entry. A script diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 9ff8ee5..1bf374f 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -107,7 +107,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. +Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. ```yaml Type: Object @@ -140,7 +140,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. +Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index d85ab86..b3d8374 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -125,7 +125,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. +Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Get-ChildItem2.md b/Docs/Cmdlets/Get-ChildItem2.md index 5c11323..4e140ae 100644 --- a/Docs/Cmdlets/Get-ChildItem2.md +++ b/Docs/Cmdlets/Get-ChildItem2.md @@ -148,9 +148,9 @@ For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. -`Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. +`Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. -Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. +Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. @@ -333,7 +333,7 @@ The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" w For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. -PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. +PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk (the pattern of a single dot returns every item), and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index 4520c73..c384ade 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -105,7 +105,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. +Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. ```yaml Type: Object @@ -138,7 +138,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. +Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index 878ce10..fcec664 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -123,7 +123,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. +Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 3ffd866..0fb48ec 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -88,7 +88,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -104,7 +104,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -212,7 +212,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -228,7 +228,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -335,7 +335,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -467,7 +467,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -544,7 +544,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Object @@ -568,7 +568,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. Object @@ -872,7 +872,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -996,7 +996,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -1340,7 +1340,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. Object @@ -3523,8 +3523,8 @@ PS C:\> Disable-Privileges Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. - `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. - Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3732,8 +3732,8 @@ PS C:\> Disable-Privileges Specifies a name pattern that an item must match to be returned. The pattern follows the wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions, on the real name of the item, not its 8.3 short name. An asterisk stands for a run of any characters, and a question mark stands for one character that is not a dot, or for nothing where a dot or the end of the name follows. A dot stands for a literal dot, or for nothing where only wildcards or the end of the pattern follow. The match ignores case and is not affected by the culture of the session. Any other character stands for itself, including a bracket, so `Report[1].txt` returns the file of that name. For example, `Report.*` also returns the item `Report`, which has no dot. A pattern of an asterisk, a dot, and an asterisk returns every item, also an item without a dot in its name, as `Get-ChildItem` does for this pattern. - `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot, and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. - Two cases are not like Windows PowerShell 5.1: a blank at the end of the pattern is part of it, where Windows PowerShell 5.1 drops it, and a pattern that ends in two dots is accepted, where Windows PowerShell 5.1 stops with an error. + `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions, so the result does not depend on the edition. The built-in `Get-ChildItem -Filter` of PowerShell 7 differs for some patterns, for example for a pattern that ends in a dot, for which it returns nothing unless an asterisk comes right before the dot (the pattern of a single dot returns every item), and for a pattern in which an asterisk is followed by a dot and question marks, for which it ignores the dot and the question marks. + Two kinds of pattern are not like Windows PowerShell 5.1: blanks at the end of the pattern, also before a final dot, are part of it, where Windows PowerShell 5.1 drops them, and a pattern that ends in an even number of dots, such as two, is accepted, where Windows PowerShell 5.1 stops with an error. The default value is `*`, which returns every item, and an empty value also returns every item. The pattern is applied to the name of each item, not to its path, and during a recursive listing it restricts only the returned items; the cmdlet still descends into every subfolder. String @@ -3879,7 +3879,7 @@ PS C:\> Disable-Privileges Before 5.0.0, `-Filter` read a bracket as the start of a character class, so a file with brackets in its name, such as `Report[1].txt`, was not returned for its name, and a pattern of an asterisk, a dot, and an asterisk dropped the items without a dot in their names, most folders among them. The cmdlet enumerates broadly and matches each name once, with the Win32 "DOS" wildcard rules that Windows PowerShell 5.1 applies to `Get-ChildItem -Filter`, in both editions: a pattern that ends in a dot returns the names without a dot, and an empty value returns every item, the same as a plain asterisk. For example, `Report.*` also returns the item `Report`, which has no dot. A star in front of a dot token may cross every dot in the name but the last one, so a pattern such as a star followed by a dot also reaches the final segment of a name with two or more dots, for example `archive.tar.gz`. - PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk, and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. + PowerShell 7 itself differs from Windows PowerShell 5.1 here: its own `Get-ChildItem -Filter` returns nothing for a pattern that ends in a dot, unless the character right before the dot is an asterisk (the pattern of a single dot returns every item), and it ignores the dot and the question marks of a pattern in which an asterisk is followed by a dot and question marks. `Get-ChildItem2` follows the Windows PowerShell 5.1 rules in both editions instead of the edition it runs in, so this is a deliberate difference from the built-in `Get-ChildItem` in PowerShell 7 for these and some similar patterns. Before 5.0.0, a `break`, a `continue`, or a `throw` in a later command of the pipeline did not end the cmdlet for an item below the first folder, also when the later command took the error of a folder that the cmdlet cannot read, for example with `2>&1`. @@ -7812,7 +7812,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -7828,7 +7828,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -7947,7 +7947,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -7963,7 +7963,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8081,7 +8081,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -8223,7 +8223,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Allow @@ -8310,7 +8310,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another one than any that you named. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. Object @@ -8334,7 +8334,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. Object @@ -8652,7 +8652,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8787,7 +8787,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -9164,7 +9164,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or an array of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values and select another scope than any that you named. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. Object From da41a8e3ee763a02f8c6184e19fd0da7bfba2e28 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 17:03:56 +0000 Subject: [PATCH 31/36] fix(access): end a collection that holds itself and name the typo in the message of a list The check that rejects a list for -AccessType and -AppliesTo opened every level of a collection of one element, so that a collection that holds itself never returned. It opens at most 32 levels now and counts a deeper nesting as a list, and it stops reading a collection after the second element, so that a long or endless sequence is not read to its end. The message names what the caller wrote: the one element of a collection of one element (also when that element is a collection itself), the first ten elements of a longer collection followed by "...", and otherwise the text of the value. Before, a collection of one misspelled name was named by its type. Tests, written first: 28 rows failed on the previous build in all four configurations and none fails now. They cover an array of one misspelled name, a collection nested 40 levels deep for both parameters of both cmdlets, and an array that holds an array of one name, which stays valid. The assertion of the value text expects "The value ''", because the old pattern could also match the name of a type. The comments say that PowerShell combines the numbers of a list bit by bit, which is what it does, and not that it adds them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 118 ++++++++++++++++++++------- Tests/ParameterSets.Access.Tests.ps1 | 26 ++++-- Tests/ParameterSets.Audit.Tests.ps1 | 21 +++-- 3 files changed, 126 insertions(+), 39 deletions(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index 738bf21..11ee2fa 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -17,10 +17,10 @@ namespace NTFSSecurity /// /// Converts the raw value of a parameter to T, the way the PowerShell binder converts an argument for a /// parameter of that type: names in any case, a comma-separated list for a flags enumeration, a number, or - /// an enumeration instance. An enumeration that is not a flags enumeration takes one value: PowerShell would - /// add up the numbers of a list for it, so that one of the values wins silently or a value that the caller - /// did not name is chosen, so a list is rejected for it, in whatever kind of collection it comes. A - /// collection of one element converts as that element. + /// an enumeration instance. An enumeration that is not a flags enumeration takes one value: PowerShell + /// combines the numbers of a list for it bit by bit, so that one of the values wins silently or a value that + /// the caller did not name is chosen, so a list is rejected for it, in whatever kind of collection it comes. + /// A collection of one element converts as that element. /// /// /// The raw value of the parameter. Not null: the caller keeps the default of the parameter for null instead @@ -40,13 +40,13 @@ namespace NTFSSecurity if (!typeof(T).IsDefined(typeof(FlagsAttribute), false)) { - string list; - convertible = TakeOneValue(value, out list); - if (list != null) + bool isList; + convertible = TakeOneValue(value, out isList); + if (isList) { throw new ArgumentException(string.Format( "The value '{0}' for parameter -{1} is not valid: the parameter takes one value, not a list. {2}", - list, parameterName, DescribeValidValues(typeof(T)))); + Describe(value), parameterName, DescribeValidValues(typeof(T)))); } } @@ -64,7 +64,7 @@ namespace NTFSSecurity catch (Exception ex) { throw new ArgumentException( - string.Format("The value '{0}' for parameter -{1} is not valid. {2}", value, parameterName, DescribeValidValues(typeof(T))), + string.Format("The value '{0}' for parameter -{1} is not valid. {2}", Describe(value), parameterName, DescribeValidValues(typeof(T))), ex); } } @@ -82,30 +82,31 @@ namespace NTFSSecurity return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); } + // How many collections that hold one collection each are opened before the argument counts as a list: enough + // for any real argument, and a stop for a collection that holds itself. + private const int MaxNesting = 32; + // The one value that the conversion takes from the argument of an enumeration that is not a flags enumeration: // the argument itself, or the only element of a collection of any kind (a wrapped PSObject counts as what it - // wraps, and a collection that holds a collection is opened too). When the argument holds more than one - // value, list is its text for the message: a string that holds a comma, a collection of several elements, or - // a collection of one element that holds such a list; the returned value is then not used. A dictionary, an - // empty collection, or a collection that cannot be enumerated is returned as it is, for the conversion to - // reject or convert with its own error. - private static object TakeOneValue(object value, out string list) + // wraps, and a collection that holds a collection is opened too). isList is true when the argument holds more + // than one value: a string that holds a comma, a collection of several elements (two are enough to know, so + // a long or endless sequence is not read to its end), a collection of one element that holds such a list, or + // collections nested deeper than MaxNesting; the returned value is then not used. A dictionary, an empty + // collection, or a collection that cannot be enumerated is returned as it is, for the conversion to reject or + // convert with its own error. + private static object TakeOneValue(object value, out bool isList) { - list = null; + isList = false; var current = value; - while (true) + for (var depth = 0; depth < MaxNesting; depth++) { var unwrapped = current is PSObject ? ((PSObject)current).BaseObject : current; var text = unwrapped as string; if (text != null) { - if (text.IndexOf(',') >= 0) - { - list = text; - } - + isList = text.IndexOf(',') >= 0; return unwrapped; } @@ -115,12 +116,19 @@ namespace NTFSSecurity return unwrapped; } - var items = new System.Collections.Generic.List(); + var count = 0; + object only = null; try { foreach (var item in collection) { - items.Add(item); + count++; + if (count > 1) + { + break; + } + + only = item; } } catch (Exception) @@ -128,19 +136,71 @@ namespace NTFSSecurity return value; } - if (items.Count == 1) + if (count == 1) { - current = items[0]; + current = only; continue; } - if (items.Count > 1) + isList = count > 1; + return value; + } + + isList = true; + return value; + } + + // The value as a message names it: a collection of one element by that element (also when it is a collection + // itself), a collection of several elements by its first ten, anything else by its text. + private static string Describe(object value) + { + var current = value; + + for (var depth = 0; depth < MaxNesting; depth++) + { + var unwrapped = current is PSObject ? ((PSObject)current).BaseObject : current; + var collection = unwrapped as IEnumerable; + + if (unwrapped is string || collection == null || unwrapped is IDictionary) { - list = string.Join(", ", items.ConvertAll(item => string.Format("{0}", item))); + return string.Format("{0}", current); } - return value; + var shown = new System.Collections.Generic.List(); + var more = false; + try + { + foreach (var item in collection) + { + if (shown.Count == 10) + { + more = true; + break; + } + + shown.Add(item); + } + + if (shown.Count == 1 && !more) + { + current = shown[0]; + continue; + } + + if (shown.Count == 0) + { + return string.Format("{0}", current); + } + + return string.Join(", ", shown.ConvertAll(item => string.Format("{0}", item))) + (more ? ", ..." : string.Empty); + } + catch (Exception) + { + return string.Format("{0}", value); + } } + + return string.Format("{0}", value); } // The valid names of the enumeration, in declaration order, with a note for a flags enumeration that a diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 6bdbb64..4a0a2c6 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1337,12 +1337,22 @@ Describe 'Get-NTFSEffectiveAccess' { # piped value the binder cannot convert to the enumeration is an error instead of a silently kept default. Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationFlags, and AppliesTo' { BeforeDiscovery { + # A collection nested 40 levels deep is a list too: the conversion opens at most 32 levels, which also ends a + # collection that holds itself (a test with such a collection would not end if the limit were lost). + $accessDeepAccessType = 'Deny' + $accessDeepScope = 'FilesOnly' + foreach ($level in 1..40) { + $accessDeepAccessType = , $accessDeepAccessType + $accessDeepScope = , $accessDeepScope + } + $accessBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } @{ Shape = 'a blank string'; Value = ''; ValueText = $null } @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } ) $accessTypeBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Denyy'; ValueText = 'Denyy' } @@ -1350,26 +1360,29 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Denyy'); ValueText = 'Denyy' } @{ Shape = 'a comma list of two names'; Value = 'Allow,Deny'; ValueText = 'Allow,Deny' } @{ Shape = 'an array of two names'; Value = @('Allow', 'Deny'); ValueText = 'Allow, Deny' } @{ Shape = 'an array of one comma list'; Value = @('Allow,Deny'); ValueText = 'Allow,Deny' } @{ Shape = 'an array that holds an array of two names'; Value = , @('Allow', 'Deny'); ValueText = 'Allow, Deny' } @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Allow', 'Deny')); ValueText = $null } @{ Shape = 'the keys of a hashtable'; Value = (@{ Allow = 1; Deny = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $accessDeepAccessType; ValueText = $null } ) - # AccessType and AppliesTo name one value each, not a combination: PowerShell would add the numbers of a list, so that - # one of the values wins silently or, for AppliesTo, a scope that the caller did not name is chosen. Every kind of - # collection counts, also a collection of one element that holds a list. + # AccessType and AppliesTo name one value each, not a combination: PowerShell combines the numbers of a list bit by + # bit, so that one of the values wins silently or, for AppliesTo, a scope that the caller did not name is chosen. + # Every kind of collection counts, also a collection of one element that holds a list. $accessAppliesToListShapes = @( @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $accessDeepScope; ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (11 + 3 * 5 + 5) = 62 rows. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (13 + 3 * 6 + 6) = 74 rows. $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { foreach ($shapeCase in $accessTypeBadShapes) { @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } @@ -1409,6 +1422,8 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = $accessDeepAccessType } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $accessDeepScope } } # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must @@ -1429,6 +1444,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Description = 'an array of one name'; Value = @('Deny') } @{ Description = 'a list of one name'; Value = [System.Collections.Generic.List[string]]::new([string[]]@('Deny')) } @{ Description = 'a hash set of one name'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('Deny')) } + @{ Description = 'an array that holds an array of one name'; Value = , @('Deny') } ) $accessValidFlagsShapeCases = @( @@ -1527,7 +1543,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF $pipelineErrors[0].TargetObject | Should -Be $path $pipelineErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) if ($ValueText) { - $pipelineErrors[0].Exception.Message | Should -Match ([regex]::Escape($ValueText)) + $pipelineErrors[0].Exception.Message | Should -Match ("The value '" + [regex]::Escape($ValueText) + "'") } (Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $before $result | Should -Not -BeNullOrEmpty diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index 6361a2a..5207d18 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1004,12 +1004,20 @@ Describe 'Get-NTFSOrphanedAudit' { # privilege, unlike most of the tests above. Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationFlags, and AppliesTo' { BeforeDiscovery { + # A collection nested 40 levels deep is a list too: the conversion opens at most 32 levels, which also ends a + # collection that holds itself. + $auditDeepScope = 'FilesOnly' + foreach ($level in 1..40) { + $auditDeepScope = , $auditDeepScope + } + $auditBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } @{ Shape = 'a blank string'; Value = ''; ValueText = $null } @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } ) $auditFlagsBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Failurre'; ValueText = 'Failurre' } @@ -1017,18 +1025,20 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Shape = 'a white-space string'; Value = ' '; ValueText = $null } @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } + @{ Shape = 'an array of one misspelled name'; Value = @('Failurre'); ValueText = 'Failurre' } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (5 + 3 * 5 + 5) = 50 rows. - # AppliesTo names one value, not a combination: PowerShell would add the numbers of a list, so that one of the - # values wins silently or a scope that the caller did not name is chosen. AuditFlags and the two flags parameters - # take a list. Every kind of collection counts, also a collection of one element that holds a list. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (6 + 3 * 6 + 6) = 60 rows. + # AppliesTo names one value, not a combination: PowerShell combines the numbers of a list bit by bit, so that one + # of the values wins silently or a scope that the caller did not name is chosen. AuditFlags and the two flags + # parameters take a list. Every kind of collection counts, also a collection of one element that holds a list. $auditAppliesToListShapes = @( @{ Shape = 'a comma list of two names'; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles'; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'an array of two names'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' } @{ Shape = 'an array of one comma list'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles'); ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' } @{ Shape = 'a hash set of two names'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')); ValueText = $null } @{ Shape = 'the keys of a hashtable'; Value = (@{ ThisFolderAndSubfolders = 1; ThisFolderAndFiles = 2 }).Keys; ValueText = $null } + @{ Shape = 'an array nested 40 levels deep'; Value = $auditDeepScope; ValueText = $null } ) $auditInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAudit', 'Remove-NTFSAudit') { @@ -1066,6 +1076,7 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders', 'ThisFolderAndFiles') } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $auditDeepScope } } # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a @@ -1181,7 +1192,7 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF $invalidValueErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' $invalidValueErrors[0].Exception.Message | Should -Match ([regex]::Escape($Parameter)) if ($ValueText) { - $invalidValueErrors[0].Exception.Message | Should -Match ([regex]::Escape($ValueText)) + $invalidValueErrors[0].Exception.Message | Should -Match ("The value '" + [regex]::Escape($ValueText) + "'") } if ($canReadAudit) { Get-TestAuditSddl -Path $path | Should -BeExactly $before From de67883cd949ead4dd0bcb37658cd649b521d1ac Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 17:03:56 +0000 Subject: [PATCH 32/36] docs: say that PowerShell combines the numbers of a list bit by bit The pages of -AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit, and the changelog, said that PowerShell adds up the numbers of a list for an enumeration that is not a flags enumeration. It combines them bit by bit (a logical or), which is why one of the values wins silently or a scope that nobody named is selected. The help file is regenerated from the pages, and the pages and the file are a fixpoint after a rebuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 4 +- Docs/Cmdlets/Add-NTFSAccess.md | 4 +- Docs/Cmdlets/Add-NTFSAudit.md | 2 +- Docs/Cmdlets/Remove-NTFSAccess.md | 4 +- Docs/Cmdlets/Remove-NTFSAudit.md | 2 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 44 ++++++++++---------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a04b2f..b49427e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -345,8 +345,8 @@ The format is based on of a parameter binding error; `-AppliesTo $null` keeps the error of the binder, because the parameter is mandatory. `-AccessType` and `-AppliesTo` take one value: a comma list or a collection of several values is an - `InvalidParameterValue` error, where PowerShell added up the numbers of the - values, so that `-AppliesTo 'ThisFolderOnly,FilesOnly'` created a + `InvalidParameterValue` error, where PowerShell combined the numbers of the + values bit by bit, so that `-AppliesTo 'ThisFolderOnly,FilesOnly'` created a `FilesOnly` entry and `-AccessType 'Allow,Deny'` a `Deny` entry. A script block, such as `-AccessType { 'Deny' }`, is no longer evaluated for every piped object and is a bad value. `Get-Help` shows the type `Object`, and diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 1bf374f..19871b7 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -107,7 +107,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. +Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. ```yaml Type: Object @@ -140,7 +140,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. +Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Add-NTFSAudit.md b/Docs/Cmdlets/Add-NTFSAudit.md index b3d8374..7d9d4d2 100644 --- a/Docs/Cmdlets/Add-NTFSAudit.md +++ b/Docs/Cmdlets/Add-NTFSAudit.md @@ -125,7 +125,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. +Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index c384ade..f10b697 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -105,7 +105,7 @@ Accept wildcard characters: False ### -AccessType -Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. +Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. ```yaml Type: Object @@ -138,7 +138,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. +Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/Docs/Cmdlets/Remove-NTFSAudit.md b/Docs/Cmdlets/Remove-NTFSAudit.md index fcec664..154b4c4 100644 --- a/Docs/Cmdlets/Remove-NTFSAudit.md +++ b/Docs/Cmdlets/Remove-NTFSAudit.md @@ -123,7 +123,7 @@ Accept wildcard characters: False ### -AppliesTo -Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. +Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ```yaml Type: Object diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 0fb48ec..7a17a77 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -88,7 +88,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -104,7 +104,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -212,7 +212,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -228,7 +228,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -335,7 +335,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -467,7 +467,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -544,7 +544,7 @@ AccessType - Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether the ACE allows or denies the rights in `-AccessRights`. The default is `Allow`. A `Deny` ACE takes precedence over `Allow` ACEs that grant the same rights. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Object @@ -568,7 +568,7 @@ AppliesTo - Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the ACE in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same ACE. The values ending in `OneLevel` limit inheritance to the direct children of the folder. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. Object @@ -872,7 +872,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -996,7 +996,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -1340,7 +1340,7 @@ AppliesTo - Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. `ThisFolderOnly` audits the folder itself, `ThisFolderSubfoldersAndFiles` audits the folder and everything below it, `SubfoldersAndFilesOnly` audits the content but not the folder itself, and the values ending in `OneLevel` limit inheritance to the direct children. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. Object @@ -7812,7 +7812,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -7828,7 +7828,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -7947,7 +7947,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -7963,7 +7963,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8081,7 +8081,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -8223,7 +8223,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Allow @@ -8310,7 +8310,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AccessType - Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently. + Specifies whether an `Allow` or a `Deny` entry is addressed. The default is `Allow`. An entry of the other type is not touched. A value that cannot be converted to `Allow` or `Deny`, including a blank one, is an `InvalidParameterValue` error: terminating when `-AccessType` is named on the command line, and reported for just that item, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently. Object @@ -8334,7 +8334,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the entry that is addressed, in the wording of the Windows security dialog, for example `ThisFolderOnly`, `ThisFolderAndSubfolders`, or `SubfoldersAndFilesOnly`. The cmdlet translates the value into the equivalent inheritance and propagation flags, so this parameter and the pair `-InheritanceFlags` and `-PropagationFlags` are two ways to describe the same entry. Use the scope that `Get-NTFSAccess` shows in the "Applies to" column of the entry. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. Object @@ -8652,7 +8652,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -8787,7 +8787,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. ThisFolderOnly @@ -9164,7 +9164,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor AppliesTo - Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would add up the numbers of the values, so that one of them wins silently or a scope that you did not name is selected. + Specifies the scope of the audit entry to remove with a single value instead of the `-InheritanceFlags` and `-PropagationFlags` pair, in the same wording the Advanced Security Settings dialog uses. The value must describe the entry as `Get-NTFSAudit` reports it, otherwise nothing is removed. Without `-AppliesTo`, the cmdlet uses `-InheritanceFlags` and `-PropagationFlags`, whose defaults describe `ThisFolderSubfoldersAndFiles`. A value that cannot be converted to one of these scopes, including a blank one, is an `InvalidParameterValue` error: terminating when `-AppliesTo` is named on the command line, and reported for just that path or descriptor, unchanged, when it comes from the pipeline. The parameter takes one value: a comma list or a collection of several values is an `InvalidParameterValue` error too, because PowerShell would combine the numbers of the values bit by bit, so that one of them wins silently or a scope that you did not name is selected. Object From ec58a18c28d11dbbeca2753474fe73d4d870647c Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 17:41:53 +0000 Subject: [PATCH 33/36] fix(access): build the message of a bad parameter value even when the text of the value throws The message of the error is built inside the catch block of the conversion, and Describe formatted the one element of a collection of one element outside of any guard. An element whose ToString throws (a script method, for example) ended the command with a terminating ExtendedTypeSystemException instead of the InvalidParameterValue error for that item, and a piped command stopped at that object, where the pages say that the error is reported for just that item. Describe now falls back to the type name whatever the text or the enumeration of the value does, for the five parameters of the four cmdlets alike. Tests, written first: 24 rows failed on the previous build in all four configurations and none fails now (an array of one object whose ToString throws, piped for every parameter and named for one parameter of each kind). New rows also pin what the previous commit claimed but did not test: the message names the first ten elements of a list and ends with an ellipsis from the eleventh, and a sequence of 100,000 elements is read only up to its first elements (a counting sequence, named and piped). A build without the two limits fails 4 of them and a build with a limit of nine fails 6, in all four configurations. The comment of the nesting limit now says exactly what it counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 59 ++++++++++++++++------------ Tests/ParameterSets.Access.Tests.ps1 | 52 +++++++++++++++++++++++- Tests/ParameterSets.Audit.Tests.ps1 | 10 ++++- 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index 11ee2fa..e6a4a01 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -82,8 +82,9 @@ namespace NTFSSecurity return string.Format("The value for parameter -{0} is null. {1}", parameterName, DescribeValidValues(typeof(T))); } - // How many collections that hold one collection each are opened before the argument counts as a list: enough - // for any real argument, and a stop for a collection that holds itself. + // The most levels of nested collections that the conversion looks into, counting the argument itself: a value + // that is wrapped in more of them counts as a list. Enough for any real argument, and a stop for a collection + // that holds itself. private const int MaxNesting = 32; // The one value that the conversion takes from the argument of an enumeration that is not a flags enumeration: @@ -151,8 +152,23 @@ namespace NTFSSecurity } // The value as a message names it: a collection of one element by that element (also when it is a collection - // itself), a collection of several elements by its first ten, anything else by its text. + // itself), a collection of several elements by its first ten, anything else by its text. The message is built + // inside a catch block, so this method must not fail whatever the text or the enumeration of a value does; a + // value that cannot be formatted is named by its type. private static string Describe(object value) + { + try + { + return DescribeValue(value); + } + catch (Exception) + { + var unwrapped = value is PSObject ? ((PSObject)value).BaseObject : value; + return unwrapped == null ? string.Empty : unwrapped.GetType().FullName; + } + } + + private static string DescribeValue(object value) { var current = value; @@ -168,36 +184,29 @@ namespace NTFSSecurity var shown = new System.Collections.Generic.List(); var more = false; - try + foreach (var item in collection) { - foreach (var item in collection) - { - if (shown.Count == 10) - { - more = true; - break; - } - - shown.Add(item); - } - - if (shown.Count == 1 && !more) + if (shown.Count == 10) { - current = shown[0]; - continue; + more = true; + break; } - if (shown.Count == 0) - { - return string.Format("{0}", current); - } + shown.Add(item); + } - return string.Join(", ", shown.ConvertAll(item => string.Format("{0}", item))) + (more ? ", ..." : string.Empty); + if (shown.Count == 1 && !more) + { + current = shown[0]; + continue; } - catch (Exception) + + if (shown.Count == 0) { - return string.Format("{0}", value); + return string.Format("{0}", current); } + + return string.Join(", ", shown.ConvertAll(item => string.Format("{0}", item))) + (more ? ", ..." : string.Empty); } return string.Format("{0}", value); diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 4a0a2c6..5693ee4 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1346,6 +1346,10 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF $accessDeepScope = , $accessDeepScope } + # The message of the error names the value, and building it must not fail when the text of an element does. + $accessThrowingElement = [pscustomobject]@{ Unrelated = 1 } + $accessThrowingElement | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString throws' } -Force + $accessBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } @{ Shape = 'a blank string'; Value = ''; ValueText = $null } @@ -1353,6 +1357,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($accessThrowingElement); ValueText = $null } ) $accessTypeBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Denyy'; ValueText = 'Denyy' } @@ -1361,6 +1366,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } @{ Shape = 'an array of one misspelled name'; Value = @('Denyy'); ValueText = 'Denyy' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($accessThrowingElement); ValueText = $null } @{ Shape = 'a comma list of two names'; Value = 'Allow,Deny'; ValueText = 'Allow,Deny' } @{ Shape = 'an array of two names'; Value = @('Allow', 'Deny'); ValueText = 'Allow, Deny' } @{ Shape = 'an array of one comma list'; Value = @('Allow,Deny'); ValueText = 'Allow,Deny' } @@ -1382,7 +1388,7 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Shape = 'an array nested 40 levels deep'; Value = $accessDeepScope; ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (13 + 3 * 6 + 6) = 74 rows. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (14 + 3 * 7 + 6) = 82 rows. $accessInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { foreach ($shapeCase in $accessTypeBadShapes) { @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Shape = $shapeCase.Shape; Value = $shapeCase.Value; ValueText = $shapeCase.ValueText } @@ -1397,6 +1403,15 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } + # The message names a list by its first ten elements: ten are named in full, eleven end with an ellipsis. The + # elements of a flags parameter are converted one by one, so these numbers fail there too (4 is not a flag). + $accessInvalidParameterCases += @( + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } + ) + $accessSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = 'Denyy'; ValueText = 'Denyy' } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = 'Typoo'; ValueText = 'Typoo' } @@ -1424,6 +1439,8 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = $accessDeepAccessType } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $accessDeepScope } + @{ Cmdlet = $cmdlet; Parameter = 'AccessType'; Value = @($accessThrowingElement) } + @{ Cmdlet = $cmdlet; Parameter = 'InheritanceFlags'; Value = @($accessThrowingElement) } } # A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must @@ -1722,6 +1739,39 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } + # The conversion reads at most two elements of a sequence to know that it is a list, and the message names ten, so a + # sequence that is long (or has no end) is not read to its end. + Context 'A long sequence of values' { + It 'Should read only the first elements of a long sequence given as -AccessType, and name its first ten in the error' -ForEach @( + @{ Mode = 'named' } + @{ Mode = 'piped' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'LongSequence' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $counter = @{ Reads = 0 } + $count = { $counter.Reads++; 'Allow' }.GetNewClosure() + $sequence = [System.Linq.Enumerable]::Select([int[]] (1..100000), [Func[int, string]] $count) + + $message = $null + if ($Mode -eq 'named') { + try { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop + } + catch { + $message = $_.Exception.Message + } + } + else { + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $sequence } | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + $message = @($pipelineErrors)[0].Exception.Message + } + + $message | Should -Match ('The value ''' + ('Allow, ' * 10) + '\.\.\.''') + $counter.Reads | Should -BeLessThan 50 + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + # A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepts it, so the # cmdlet must not keep the default silently. A named $null is reported the same way as a bad named value. diff --git a/Tests/ParameterSets.Audit.Tests.ps1 b/Tests/ParameterSets.Audit.Tests.ps1 index 5207d18..6c4aab4 100644 --- a/Tests/ParameterSets.Audit.Tests.ps1 +++ b/Tests/ParameterSets.Audit.Tests.ps1 @@ -1011,6 +1011,10 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF $auditDeepScope = , $auditDeepScope } + # The message of the error names the value, and building it must not fail when the text of an element does. + $auditThrowingElement = [pscustomobject]@{ Unrelated = 1 } + $auditThrowingElement | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString throws' } -Force + $auditBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Typoo'; ValueText = 'Typoo' } @{ Shape = 'a blank string'; Value = ''; ValueText = $null } @@ -1018,6 +1022,7 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } @{ Shape = 'an array of one misspelled name'; Value = @('Typoo'); ValueText = 'Typoo' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($auditThrowingElement); ValueText = $null } ) $auditFlagsBadShapes = @( @{ Shape = 'a misspelled name'; Value = 'Failurre'; ValueText = 'Failurre' } @@ -1026,9 +1031,10 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Shape = 'an undefined number'; Value = 99; ValueText = '99' } @{ Shape = 'a hashtable'; Value = @{ Unrelated = 1 }; ValueText = $null } @{ Shape = 'an array of one misspelled name'; Value = @('Failurre'); ValueText = 'Failurre' } + @{ Shape = 'an array of one object whose ToString throws'; Value = @($auditThrowingElement); ValueText = $null } ) - # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (6 + 3 * 6 + 6) = 60 rows. + # The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (7 + 3 * 7 + 6) = 68 rows. # AppliesTo names one value, not a combination: PowerShell combines the numbers of a list bit by bit, so that one # of the values wins silently or a scope that the caller did not name is chosen. AuditFlags and the two flags # parameters take a list. Every kind of collection counts, also a collection of one element that holds a list. @@ -1077,6 +1083,8 @@ Describe 'Invalid and valid values of AuditFlags, InheritanceFlags, PropagationF @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @('ThisFolderAndSubfolders,ThisFolderAndFiles') } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = [System.Collections.Generic.HashSet[string]]::new([string[]]@('ThisFolderAndSubfolders', 'ThisFolderAndFiles')) } @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = $auditDeepScope } + @{ Cmdlet = $cmdlet; Parameter = 'AuditFlags'; Value = @($auditThrowingElement) } + @{ Cmdlet = $cmdlet; Parameter = 'AppliesTo'; Value = @($auditThrowingElement) } } # A named $null (a literal $null, a variable, splatting, or $PSDefaultParameterValues) must stay a From ce34395a8b798b0cec3eeb48ee795fdeca592a9a Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 17:59:55 +0000 Subject: [PATCH 34/36] fix(access): name a value that cannot be formatted by the short form of its type The fallback of the message of a bad parameter value used the full name of the type. For a generic type that spells out the assembly of every type argument, so a LINQ iterator whose selector throws gave a message of 330 characters, where the code before the last commit printed the short form. The fallback uses Type.ToString now: System.Collections.Generic.List`1[System.Object]. Two rows pin the short form (a generic list of one object whose ToString throws, for AccessType and InheritanceFlags of Add-NTFSAccess, piped). They failed on the previous commit in all four configurations and pass now. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 4 ++-- Tests/ParameterSets.Access.Tests.ps1 | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index e6a4a01..dc45ed5 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -154,7 +154,7 @@ namespace NTFSSecurity // The value as a message names it: a collection of one element by that element (also when it is a collection // itself), a collection of several elements by its first ten, anything else by its text. The message is built // inside a catch block, so this method must not fail whatever the text or the enumeration of a value does; a - // value that cannot be formatted is named by its type. + // value that cannot be formatted is named by its type, in the short form of Type.ToString. private static string Describe(object value) { try @@ -164,7 +164,7 @@ namespace NTFSSecurity catch (Exception) { var unwrapped = value is PSObject ? ((PSObject)value).BaseObject : value; - return unwrapped == null ? string.Empty : unwrapped.GetType().FullName; + return unwrapped == null ? string.Empty : unwrapped.GetType().ToString(); } } diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 5693ee4..8136b65 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1404,12 +1404,15 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } # The message names a list by its first ten elements: ten are named in full, eleven end with an ellipsis. The - # elements of a flags parameter are converted one by one, so these numbers fail there too (4 is not a flag). + # elements of a flags parameter are converted one by one, so these numbers fail there too (4 is not a flag). A + # value whose text cannot be built is named by its type, in the short form that Type.ToString gives. $accessInvalidParameterCases += @( @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'ten numbers'; Value = 1..10; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' } @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'eleven numbers'; Value = 1..11; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'AccessType'; Shape = 'a generic list of one object whose ToString throws'; Value = [System.Collections.Generic.List[object]]::new([object[]] @($accessThrowingElement)); ValueText = 'System.Collections.Generic.List`1[System.Object]' } + @{ Cmdlet = 'Add-NTFSAccess'; Parameter = 'InheritanceFlags'; Shape = 'a generic list of one object whose ToString throws'; Value = [System.Collections.Generic.List[object]]::new([object[]] @($accessThrowingElement)); ValueText = 'System.Collections.Generic.List`1[System.Object]' } ) $accessSdInvalidParameterCases = foreach ($cmdlet in 'Add-NTFSAccess', 'Remove-NTFSAccess') { From b436423ab832678db25bded71c55d7e2a5b4a63b Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 18:03:56 +0000 Subject: [PATCH 35/36] fix(access): count a sequence as a list when its enumerator fails after the second element The check of a list for -AccessType and -AppliesTo stops reading a collection after the second element. The loop then releases the enumerator, and when that failed the catch handed the collection to the conversion of PowerShell with isList still false. PowerShell combines the numbers of a list bit by bit, so a sequence of Allow and Deny whose enumerator throws in Dispose created a Deny entry, and one of SubfoldersOnly and FilesOnly a scope that nobody named. The catch keeps what the loop knows: two elements read make a list, which is rejected like every other list. Only a custom enumerator can do this; the last review of round 2 found it. Tests, written first: 4 rows failed on the previous commit in all four configurations and none fails now (a compiled stand-in whose enumerator throws in Dispose, for AccessType and AppliesTo, named and piped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 4 +- Tests/ParameterSets.Access.Tests.ps1 | 92 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index dc45ed5..f7d1e49 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -94,7 +94,7 @@ namespace NTFSSecurity // a long or endless sequence is not read to its end), a collection of one element that holds such a list, or // collections nested deeper than MaxNesting; the returned value is then not used. A dictionary, an empty // collection, or a collection that cannot be enumerated is returned as it is, for the conversion to reject or - // convert with its own error. + // convert with its own error; one whose enumeration fails after its second element is a list all the same. private static object TakeOneValue(object value, out bool isList) { isList = false; @@ -134,6 +134,8 @@ namespace NTFSSecurity } catch (Exception) { + // The loop releases the enumerator when it stops after the second element, and that can fail. + isList = count > 1; return value; } diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 8136b65..4950ccc 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1775,6 +1775,98 @@ Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationF } } + # A list is rejected when the conversion has read its second element. The loop that reads it then releases the + # enumerator, and an enumerator that fails there must not turn the list into a value that PowerShell converts. + Context 'A sequence whose enumerator fails when it is released' { + BeforeAll { + if (-not ('NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose' -as [type])) { + Add-Type -TypeDefinition @' +namespace NtfsSecurityTests +{ + public sealed class SequenceWhoseEnumeratorFailsOnDispose : System.Collections.IEnumerable + { + private readonly object[] items; + + public SequenceWhoseEnumeratorFailsOnDispose(params object[] items) + { + this.items = items; + } + + public System.Collections.IEnumerator GetEnumerator() + { + return new Enumerator(this.items); + } + + private sealed class Enumerator : System.Collections.IEnumerator, System.IDisposable + { + private readonly object[] items; + private int index = -1; + + public Enumerator(object[] items) + { + this.items = items; + } + + public object Current + { + get { return this.items[this.index]; } + } + + public bool MoveNext() + { + this.index++; + return this.index < this.items.Length; + } + + public void Reset() + { + this.index = -1; + } + + public void Dispose() + { + throw new System.InvalidOperationException("The enumerator fails when it is released."); + } + } + } +} +'@ + } + } + + It 'Should reject a list of two values for given as , and change nothing' -ForEach @( + @{ Parameter = 'AccessType'; Mode = 'named'; Values = @('Allow', 'Deny') } + @{ Parameter = 'AccessType'; Mode = 'piped'; Values = @('Allow', 'Deny') } + @{ Parameter = 'AppliesTo'; Mode = 'named'; Values = @('SubfoldersOnly', 'FilesOnly') } + @{ Parameter = 'AppliesTo'; Mode = 'piped'; Values = @('SubfoldersOnly', 'FilesOnly') } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFails' -Directory + $before = (Get-Acl -LiteralPath $folder).Sddl + $sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose]::new([object[]] $Values) + + $caughtId = $null + if ($Mode -eq 'named') { + $namedArguments = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $namedArguments[$Parameter] = $sequence + try { + Add-NTFSAccess @namedArguments -ErrorAction Stop + } + catch { + $caughtId = $_.FullyQualifiedErrorId + } + } + else { + $properties = @{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData' } + $properties[$Parameter] = $sequence + [pscustomobject] $properties | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue + $caughtId = @($pipelineErrors)[0].FullyQualifiedErrorId + } + + $caughtId | Should -BeLike 'InvalidParameterValue,*' + (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before + } + } + # A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType # $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepts it, so the # cmdlet must not keep the default silently. A named $null is reported the same way as a bad named value. From 696ac3891224834561a0c43af485a359bbb5e26d Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Wed, 21 Oct 2026 18:18:31 +0000 Subject: [PATCH 36/36] fix(access): ignore a failure to release the enumerator when a value is read The previous commit made a sequence whose enumerator throws in Dispose after the second element count as a list, and left every other count to the catch, which hands the collection to the conversion of PowerShell. A sequence of one element whose enumerator fails in Dispose therefore got through when that element was a comma list or an array of two values: PowerShell combined the numbers bit by bit and the entry was created with a value that nobody named. The reading is done with an explicit enumerator that is released in its own guard now, so a failing release changes nothing of what was read, whatever the number of elements. The catch is back to what it was: a reading that fails leaves the collection to the conversion, which rejects it with its own error. Tests, written first: 8 rows failed on the previous commit in all four configurations and none fails now (a compiled stand-in whose enumerator throws in Dispose; a sequence of one comma list and of one array of two values, for AccessType and AppliesTo, named and piped). Two rows pin that a sequence of one valid value stays valid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- NTFSSecurity/ParameterConversion.cs | 33 +++++++++++++++++++++++---- Tests/ParameterSets.Access.Tests.ps1 | 34 ++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/NTFSSecurity/ParameterConversion.cs b/NTFSSecurity/ParameterConversion.cs index f7d1e49..56a2457 100644 --- a/NTFSSecurity/ParameterConversion.cs +++ b/NTFSSecurity/ParameterConversion.cs @@ -94,7 +94,7 @@ namespace NTFSSecurity // a long or endless sequence is not read to its end), a collection of one element that holds such a list, or // collections nested deeper than MaxNesting; the returned value is then not used. A dictionary, an empty // collection, or a collection that cannot be enumerated is returned as it is, for the conversion to reject or - // convert with its own error; one whose enumeration fails after its second element is a list all the same. + // convert with its own error. A failure to release the enumerator after the elements were read changes nothing. private static object TakeOneValue(object value, out bool isList) { isList = false; @@ -119,9 +119,11 @@ namespace NTFSSecurity var count = 0; object only = null; + IEnumerator enumerator = null; try { - foreach (var item in collection) + enumerator = collection.GetEnumerator(); + while (enumerator.MoveNext()) { count++; if (count > 1) @@ -129,15 +131,17 @@ namespace NTFSSecurity break; } - only = item; + only = enumerator.Current; } } catch (Exception) { - // The loop releases the enumerator when it stops after the second element, and that can fail. - isList = count > 1; return value; } + finally + { + Release(enumerator); + } if (count == 1) { @@ -153,6 +157,25 @@ namespace NTFSSecurity return value; } + // Releasing the enumerator of a collection that was read can fail (only a custom enumerator does), and that does + // not change what was read. + private static void Release(IEnumerator enumerator) + { + var disposable = enumerator as IDisposable; + if (disposable == null) + { + return; + } + + try + { + disposable.Dispose(); + } + catch (Exception) + { + } + } + // The value as a message names it: a collection of one element by that element (also when it is a collection // itself), a collection of several elements by its first ten, anything else by its text. The message is built // inside a catch block, so this method must not fail whatever the text or the enumeration of a value does; a diff --git a/Tests/ParameterSets.Access.Tests.ps1 b/Tests/ParameterSets.Access.Tests.ps1 index 4950ccc..d4e9a20 100644 --- a/Tests/ParameterSets.Access.Tests.ps1 +++ b/Tests/ParameterSets.Access.Tests.ps1 @@ -1834,11 +1834,15 @@ namespace NtfsSecurityTests } } - It 'Should reject a list of two values for given as , and change nothing' -ForEach @( - @{ Parameter = 'AccessType'; Mode = 'named'; Values = @('Allow', 'Deny') } - @{ Parameter = 'AccessType'; Mode = 'piped'; Values = @('Allow', 'Deny') } - @{ Parameter = 'AppliesTo'; Mode = 'named'; Values = @('SubfoldersOnly', 'FilesOnly') } - @{ Parameter = 'AppliesTo'; Mode = 'piped'; Values = @('SubfoldersOnly', 'FilesOnly') } + It 'Should reject for given as , and change nothing' -ForEach @( + foreach ($mode in 'named', 'piped') { + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of two values'; Values = @('Allow', 'Deny') } + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of one comma list'; Values = @('Allow,Deny') } + @{ Parameter = 'AccessType'; Mode = $mode; Shape = 'a sequence of one array of two values'; Values = , @('Allow', 'Deny') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of two values'; Values = @('SubfoldersOnly', 'FilesOnly') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of one comma list'; Values = @('ThisFolderOnly,FilesOnly') } + @{ Parameter = 'AppliesTo'; Mode = $mode; Shape = 'a sequence of one array of two values'; Values = , @('SubfoldersOnly', 'FilesOnly') } + } ) { $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFails' -Directory $before = (Get-Acl -LiteralPath $folder).Sddl @@ -1865,6 +1869,26 @@ namespace NtfsSecurityTests $caughtId | Should -BeLike 'InvalidParameterValue,*' (Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before } + + # What was read is what counts: a failing release does not turn one valid value into an error. + It 'Should still accept a sequence of one valid value for -AccessType given as ' -ForEach @( + @{ Mode = 'named' } + @{ Mode = 'piped' } + ) { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFailsValid' -Directory + $sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose]::new([object[]] @('Deny')) + + if ($Mode -eq 'named') { + Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop + } + else { + [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AccessType = $sequence } | Add-NTFSAccess -ErrorAction Stop + } + + $rule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) + $rule | Should -HaveCount 1 + $rule[0].AccessControlType | Should -Be ([System.Security.AccessControl.AccessControlType]::Deny) + } } # A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType