mirror of https://github.com/raandree/NTFSSecurity
Browse Source
test: close the coverage unknowns of the quality gate and fix three defectspull/126/head
committed by
GitHub
38 changed files with 8204 additions and 136 deletions
@ -0,0 +1,92 @@ |
|||
--- |
|||
status: current |
|||
last-verified: 2026-10-10 |
|||
owner: software-engineer |
|||
source: Handoff 5 (the coverage unknowns): probes, red and green runs, and the independent review |
|||
--- |
|||
|
|||
# Debugging insights |
|||
|
|||
Recurring defect classes and how to find them. Evidence is in |
|||
`Tests/Coverage/Quality-Gate-Unknowns-2026-10-10.md`; a fix needs a test that |
|||
fails without it (Decision 16). |
|||
|
|||
## Culture-sensitive comparison (`tr-TR`) |
|||
|
|||
- With `tr-TR`, `I` and `i` are different letters, so a comparison that takes |
|||
the culture of the thread drops or misses items: `ToLower` or `ToUpper`, |
|||
`Compare`, `StartsWith`, `EndsWith`, or `IndexOf(string)` without a |
|||
`StringComparison`, a case-insensitive `Regex`, a `WildcardPattern` without |
|||
`CultureInvariant` (`string.Equals(a, b)` and `==` are ordinal and safe). |
|||
Handoff 5 found two defects of this class: `IdentityReference2.Equals(object)` |
|||
called with a name, which lowercased both sides (now `OrdinalIgnoreCase`), |
|||
and `Get-ChildItem2 -Filter` (`index*` did not list `Index.txt`). |
|||
- A scan of the C# is not enough: AlphaFS 2.2 builds a culture-sensitive regex |
|||
from a search pattern. `Get-ChildItem2` therefore enumerates under |
|||
`InvariantCultureScope` (around the creation of the enumerator and each |
|||
`MoveNext`, never across a `WriteObject`) and its own `WildcardPattern` has |
|||
`CultureInvariant`. Check any new library call that takes a user pattern. |
|||
- PowerShell `-eq`, `-contains`, and `-in` do not take the thread culture; |
|||
only the .NET method call failed. Probe a claim before it enters the |
|||
changelog or a report (the first wording named `-eq` and was wrong). |
|||
- A test sets the culture of the thread, restores it in `finally`, names the |
|||
rows that differ (`index*`, `*.INI`), keeps a control (`INDEX*`), reads the |
|||
culture in the next pipeline stage for each item and once after the call (a |
|||
cmdlet that does not restore it, or writes an item inside the scope, fails |
|||
both: two mutations proved it), and is proved red on the previous build and |
|||
green on the new one in all four configurations |
|||
(`Tests/ItemCmdlets.Tests.ps1`, `Tests/ObjectApis.Tests.ps1`). |
|||
|
|||
## A test oracle that is wrong on a domain member |
|||
|
|||
- `Get-Acl -Audit` reads the SACL with the other sections and reports an item |
|||
without audit entries as protected (Windows Server 2019, 2022, and 2025 |
|||
domain members, Windows 11); the module and `Get-NTFSInheritance` read the |
|||
SACL alone. Tests read the flag with .NET `GetAccessControl(Audit)` |
|||
(`Get-TestAuditAcl`). A suite that is green on the host proves nothing about |
|||
an oracle: run a new test on a domain member before calling it done. |
|||
|
|||
## A piped property and the parameter set |
|||
|
|||
- An object with a property `AppliesTo`, piped into `Add-` or |
|||
`Remove-NTFSAccess` or `-NTFSAudit` with a path, bound the default |
|||
`PathComplex` set and lost the property. Fixed in `93ed2c7`: the four cmdlets |
|||
declare no default set, so the property selects `PathSimple`, an object with |
|||
`AppliesTo` and `InheritanceFlags` fails with `AmbiguousParameterSet`, and the |
|||
tests pipe an object of each shape (red on the previous build, green on the |
|||
new one). A probe that tells the sets uses an entry of the default scope: |
|||
removing `ThisFolderOnly` from it leaves the `InheritOnly` part. |
|||
- PowerShell restores only the parameters that the previous piped object |
|||
bound. A field that the cmdlet writes itself stays for the next object: the |
|||
scope that `ProcessRecord` converted from `AppliesTo` into the field behind |
|||
`-InheritanceFlags` went to the next object without a scope. `93ed2c7` |
|||
shipped that, the third review found it (rc7 had it for piped |
|||
`-SecurityDescriptor` objects), and `9783d2a` keeps the flags of a record in |
|||
locals. A pipeline test pipes two or more objects of different shape in one |
|||
call (`Objects with different scopes in one pipeline`, both ParameterSets |
|||
files); one object per call cannot see it. |
|||
- Probe the route that the documentation shows. `Import-Csv` of a file gives an |
|||
empty string for a blank cell, `ConvertFrom-Csv` of strings gives `$null` for |
|||
a trailing blank cell, and `Import-Csv` gives `$null` for a column that a row |
|||
lacks. The binder treats them differently: `$null` fails to bind, and a value |
|||
that it cannot convert to the enum (an empty string, a typo) is ignored, so |
|||
the object is handled as one without the property. The first statement and |
|||
the first tests used the convenient route, and the reviewer found the false |
|||
claim; the corrected claim was then too narrow once more (a value that |
|||
converts binds, whatever its form; a flag next to an unusable value wins). |
|||
Write the claim from a probe of each form, not from the one that you tried. |
|||
- A sentence that says what a test proves is a claim too: a title that says |
|||
"and change nothing" needs an assertion that can see the change (the audit |
|||
cmdlets write the SACL, not the DACL), and "cannot go red" needs a run. |
|||
- The cause was `DefaultParameterSetName = "PathComplex"`: when a piped object |
|||
satisfies the mandatory parameters of the default set, PowerShell settles on |
|||
it and never tries a parameter that belongs to another set. A stand-alone |
|||
model with the four sets and no default binds `PathSimple` for an object with |
|||
`AppliesTo`, `PathComplex` for one without, and an error for one with |
|||
`AppliesTo` and `InheritanceFlags` (`probes\p-parameterset-binding*.ps1` in |
|||
the Handoff 5 evidence; the model binds `PathComplex` for a piped descriptor |
|||
where the real cmdlets bind `SDSimple`); the real cmdlets agreed for paths |
|||
(`probes\p-proto-appliesto.ps1`). The default dated from 4.2.6, when |
|||
`AppliesTo` was optional; `4b1abc9` made it mandatory in the Simple sets, so |
|||
the default no longer decided a named call. Build a model of the parameter |
|||
sets before you change an attribute, and test the real cmdlet afterwards. |
|||
@ -0,0 +1,18 @@ |
|||
--- |
|||
status: accepted |
|||
date: 2026-10-11 |
|||
last-verified: 2026-10-11 |
|||
owner: shared |
|||
source: maintainer decision of 2026-10-11 |
|||
--- |
|||
|
|||
# Decision 25: Four environments are out of scope for the tests and the acceptance |
|||
|
|||
- Choice: The tests and the acceptance of the quality gate do not cover |
|||
Windows Server Core, ARM64, a localized operating system, or file servers |
|||
that are not Windows servers. They are not open items, and no agent plans, |
|||
deploys, or tests them (maintainer, 2026-10-11). |
|||
- Rationale: The audience of the tool does not need them (maintainer). |
|||
- Not changed: Decision 23 and #34, the question of what to do for reporters |
|||
with non-Windows file servers, stay as they are until the maintainer says |
|||
otherwise. |
|||
@ -0,0 +1,49 @@ |
|||
--- |
|||
status: accepted |
|||
date: 2026-10-11 |
|||
last-verified: 2026-10-11 |
|||
owner: shared |
|||
source: maintainer decision of 2026-10-11 |
|||
--- |
|||
|
|||
# Decision 26: Add and Remove for access and audit declare no default parameter set |
|||
|
|||
- Choice: `Add-NTFSAccess`, `Remove-NTFSAccess`, `Add-NTFSAudit`, and |
|||
`Remove-NTFSAudit` declare no `DefaultParameterSetName` (maintainer, |
|||
2026-10-11; `93ed2c7`). An object piped with a path binds `PathSimple` when |
|||
it has `AppliesTo`, and `PathComplex` when it has `InheritanceFlags`, |
|||
`PropagationFlags`, or neither (the default scope). An object that has |
|||
`AppliesTo` and one of the flags fails with `AmbiguousParameterSet`, like the |
|||
same named parameters. |
|||
- Rationale: With `PathComplex` as the default, an object that satisfied its |
|||
mandatory parameters made the binder settle on it, and a piped `AppliesTo` |
|||
was dropped without a message (a silent change of the scope). `4b1abc9` had |
|||
made `AppliesTo` mandatory in the Simple sets, so the default no longer |
|||
decided any named call. |
|||
- Consequence: a script that piped objects with `AppliesTo` and the flags |
|||
together got the flags before and gets an error now; so does an object that |
|||
has both `Path` and `SecurityDescriptor`, where the path won. An |
|||
`AppliesTo` that is `$null` fails to bind for that object (a CSV row with |
|||
fewer fields than its header has `$null` there); a call with neither |
|||
`-Path` nor `-SecurityDescriptor` fails with `AmbiguousParameterSet` instead |
|||
of a missing-parameter error (CHANGELOG, Fixed). A value that PowerShell |
|||
cannot convert to a scope (the blank cell that `Import-Csv` reads, a typo) |
|||
is ignored silently, and the object is handled as one without `AppliesTo`: |
|||
its own flags, or the default scope when it has none. The binder swallows a |
|||
value that it cannot convert, so failing loudly needs a design change |
|||
(report, open item 3). Named calls, |
|||
`-SecurityDescriptor` calls without `AppliesTo`, and |
|||
`Get-NTFSAccess | Add-NTFSAccess -Path` do not change. The syntax lists |
|||
`PathSimple` first, because PowerShell orders the sets by declaration. |
|||
- Follow-up: the third independent review found that `ProcessRecord` wrote |
|||
the scope converted from `AppliesTo` into the fields behind the flag |
|||
parameters, which PowerShell does not reset between piped objects. The flags |
|||
of a record are locals since `9783d2a`; rc7 had the defect for piped |
|||
`-SecurityDescriptor` objects. The reviewer's checks found that "an empty |
|||
`AppliesTo` fails to bind" was true for `$null` only (`7bc7925`), and that |
|||
the sentence about unconvertible values was too narrow (`2112ee3`). |
|||
- Evidence: report `Tests/Coverage/Quality-Gate-Unknowns-2026-10-10.md` |
|||
(decision 23; the section on the fix of the piped `AppliesTo`), the tests in |
|||
`Tests/ParameterSets.Access.Tests.ps1` and `Tests/ParameterSets.Audit.Tests.ps1` |
|||
(red before, green after, in four configurations), and |
|||
`.memory-bank/debugging-insights.md`. |
|||
|
|
File diff suppressed because it is too large
|
|
|
|
|
|
File diff suppressed because it is too large
@ -0,0 +1,774 @@ |
|||
<# |
|||
Tests the parameter sets of the access cmdlets (Add-NTFSAccess, Remove-NTFSAccess, Clear-NTFSAccess, |
|||
Get-NTFSAccess, Get-NTFSOrphanedAccess, Get-NTFSSimpleAccess, Get-NTFSEffectiveAccess) that the judged test matrix |
|||
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. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
|
|||
# Security2.FileSystemAccessRule2 for Add/Remove/Get-NTFSAccess/Get-NTFSOrphanedAccess/Get-NTFSEffectiveAccess, |
|||
# Security2.SimpleFileSystemAccessRule for Get-NTFSSimpleAccess; Clear-NTFSAccess declares no output type (no -PassThru). |
|||
$declaredOutputTypes = @( |
|||
@{ Name = 'Add-NTFSAccess'; Types = @('Security2.FileSystemAccessRule2') } |
|||
@{ Name = 'Remove-NTFSAccess'; Types = @('Security2.FileSystemAccessRule2') } |
|||
@{ Name = 'Get-NTFSAccess'; Types = @('Security2.FileSystemAccessRule2') } |
|||
@{ Name = 'Get-NTFSOrphanedAccess'; Types = @('Security2.FileSystemAccessRule2') } |
|||
@{ Name = 'Get-NTFSEffectiveAccess'; Types = @('Security2.FileSystemAccessRule2') } |
|||
@{ Name = 'Get-NTFSSimpleAccess'; Types = @('Security2.SimpleFileSystemAccessRule') } |
|||
) |
|||
} |
|||
|
|||
BeforeAll { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' |
|||
Import-Module -Name $modulePath -Force -ErrorAction Stop |
|||
$sandbox = New-TestSandbox -Name 'ParameterSetsAccess' |
|||
Push-Location -LiteralPath $sandbox |
|||
|
|||
$privateData = (Get-Module -Name NTFSSecurity).PrivateData |
|||
$enablePrivileges = $privateData['EnablePrivileges'] |
|||
$sidType = [System.Security.Principal.SecurityIdentifier] |
|||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value |
|||
|
|||
function Get-TestAccountScope { |
|||
# The scopes of the explicit entries of one account, each as 'inheritance flags/propagation flags', joined by ';' |
|||
param ( |
|||
[Parameter(Mandatory)] [System.Security.AccessControl.FileSystemSecurity] $Acl, |
|||
[Parameter(Mandatory)] [string] $Account |
|||
) |
|||
@($Acl.GetAccessRules($true, $false, $sidType) | |
|||
Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account } | |
|||
ForEach-Object -Process { '{0}/{1}' -f $_.InheritanceFlags, $_.PropagationFlags }) -join ';' |
|||
} |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'Add-NTFSAccess' { |
|||
Context 'PathComplex parameter set bound through the pipeline' { |
|||
It 'Should add one explicit entry for each path bound through the pipeline, by FullName and by property name, leaving the rest of each item unchanged' { |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeFirst' |
|||
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeSecond' |
|||
$third = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeThird' |
|||
$before = foreach ($path in $first, $second, $third) { |
|||
$acl = Get-Acl -LiteralPath $path |
|||
[pscustomobject]@{ Path = $path; Owner = $acl.GetOwner($sidType).Value; Inherited = @($acl.GetAccessRules($false, $true, $sidType)).Count } |
|||
} |
|||
|
|||
Get-Item2 -Path $first, $second | Add-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
[pscustomobject]@{ Path = $third; Account = 'S-1-1-0'; AccessRights = 'ReadData' } | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
foreach ($state in $before) { |
|||
$acl = Get-Acl -LiteralPath $state.Path |
|||
$acl.GetOwner($sidType).Value | Should -Be $state.Owner |
|||
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $state.Inherited |
|||
$explicit = @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) |
|||
$explicit | Should -HaveCount 1 |
|||
$explicit[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeTrue |
|||
} |
|||
} |
|||
} |
|||
|
|||
Context 'PathComplex parameter set with one denied path and -PassThru' { |
|||
BeforeAll { |
|||
# So that an elevated session meets the same denial as a basic user (see PathErrors.Tests.ps1). |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It 'Should write one AddAceError for the blocked path, leave it unchanged, and return -PassThru entries only for the path it changed' { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThruBlocked' |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThruNext' |
|||
Block-TestWritePermission -Sandbox $sandbox -Path $blocked |
|||
$before = (Get-Acl -LiteralPath $blocked).Sddl |
|||
|
|||
$result = @(Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$addErrors | Should -HaveCount 1 |
|||
$addErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' |
|||
$addErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$addErrors[0].TargetObject | Should -Be $blocked |
|||
(Get-Acl -LiteralPath $blocked).Sddl | Should -BeExactly $before |
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { |
|||
$_ | Should -BeOfType ([Security2.FileSystemAccessRule2]) |
|||
$_.FullName | Should -Be $next |
|||
} |
|||
@($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' -and -not $_.IsInherited }) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
|
|||
# The property AppliesTo selects PathSimple, InheritanceFlags and PropagationFlags select PathComplex, and an object that |
|||
# has none of them gets the default scope. No parameter set is the default: while PathComplex was, an object that had |
|||
# AppliesTo bound PathComplex as well, and the cmdlet dropped the property. |
|||
Context 'Choosing the parameter set of an object bound through the pipeline by property name' { |
|||
It 'Should give the entry for the piped object that has <Shape> the scope <Scope>' -ForEach @( |
|||
@{ Name = 'AddPipedThisFolderOnly'; Shape = 'AppliesTo ThisFolderOnly'; Extra = @{ AppliesTo = 'ThisFolderOnly' }; Scope = 'None/None' } |
|||
@{ Name = 'AddPipedSubfolders'; Shape = 'AppliesTo ThisFolderAndSubfolders'; Extra = @{ AppliesTo = 'ThisFolderAndSubfolders' }; Scope = 'ContainerInherit/None' } |
|||
@{ Name = 'AddPipedInheritOnly'; Shape = 'AppliesTo SubfoldersAndFilesOnly'; Extra = @{ AppliesTo = 'SubfoldersAndFilesOnly' }; Scope = 'ContainerInherit, ObjectInherit/InheritOnly' } |
|||
@{ Name = 'AddPipedFlags'; Shape = 'InheritanceFlags and PropagationFlags'; Extra = @{ InheritanceFlags = 'ContainerInherit'; PropagationFlags = 'InheritOnly' }; Scope = 'ContainerInherit/InheritOnly' } |
|||
@{ Name = 'AddPipedNoScope'; Shape = 'no scope'; Extra = @{}; Scope = 'ContainerInherit, ObjectInherit/None' } |
|||
) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory |
|||
|
|||
[pscustomobject] (@{ Path = $folder; Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } + $Extra) | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
Get-TestAccountScope -Acl (Get-Acl -LiteralPath $folder) -Account 'S-1-5-32-546' | Should -BeExactly $Scope |
|||
} |
|||
|
|||
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error and change nothing' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipedBoth' -Directory |
|||
$before = (Get-Acl -LiteralPath $folder).Sddl |
|||
$object = [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; InheritanceFlags = 'None' } |
|||
|
|||
{ $object | Add-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' |
|||
|
|||
(Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before |
|||
} |
|||
} |
|||
|
|||
# PowerShell resets only the parameters that the previous object bound from the pipeline. The scope that the cmdlet |
|||
# derives from AppliesTo must therefore not stay in the fields behind InheritanceFlags and PropagationFlags: the next |
|||
# object that has no scope, or only one flag, would get it. |
|||
Context 'Objects with different scopes in one pipeline' { |
|||
It 'Should give each piped path object only its own scope: AppliesTo, none, AppliesTo, one flag, none' { |
|||
$folders = @('AddMixedThisFolderOnly', 'AddMixedNoScope', 'AddMixedInheritOnly', 'AddMixedOneFlag', 'AddMixedNoScopeAgain' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$entry = @{ Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ Path = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[1] }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[2]; AppliesTo = 'SubfoldersAndFilesOnly' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[3]; InheritanceFlags = 'ContainerInherit' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[4] }) |
|||
) | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
$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|ContainerInherit/None|ContainerInherit, ObjectInherit/None' |
|||
} |
|||
|
|||
It 'Should give each row of a CSV file that Import-Csv reads and that has an AppliesTo column its own scope' { |
|||
$folders = @('AddCsvThisFolderOnly', 'AddCsvSubfolders', 'AddCsvInheritOnly' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvScopes.csv' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv |
|||
Set-Content -LiteralPath $csv -Value @( |
|||
'Path,Account,AccessRights,AppliesTo' |
|||
('"{0}",S-1-5-32-546,ReadData,ThisFolderOnly' -f $folders[0]) |
|||
('"{0}",S-1-5-32-546,ReadData,ThisFolderAndSubfolders' -f $folders[1]) |
|||
('"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders[2]) |
|||
) |
|||
|
|||
Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
$scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl (Get-Acl -LiteralPath $_) -Account 'S-1-5-32-546' } |
|||
$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' { |
|||
$folders = @('AddCsvBlankFirst', 'AddCsvBlankCell', 'AddCsvBlankLast' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvBlank.csv' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv |
|||
Set-Content -LiteralPath $csv -Value @( |
|||
'Path,Account,AccessRights,AppliesTo' |
|||
('"{0}",S-1-5-32-546,ReadData,ThisFolderOnly' -f $folders[0]) |
|||
('"{0}",S-1-5-32-546,ReadData,' -f $folders[1]) |
|||
('"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders[2]) |
|||
) |
|||
|
|||
Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
$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' |
|||
} |
|||
|
|||
# $null cannot be bound at all (ConvertFrom-Csv makes it of a trailing blank cell): the object fails, loudly, and the cmdlet goes |
|||
# on with the next one. |
|||
It 'Should report a binding error for the piped object whose AppliesTo is $null, leave its folder alone, and still process the other objects' { |
|||
$folders = @('AddNullFirst', 'AddNullMiddle', 'AddNullLast' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$entry = @{ Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ Path = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[1]; AppliesTo = $null }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[2]; AppliesTo = 'SubfoldersAndFilesOnly' }) |
|||
) | Add-NTFSAccess -ErrorVariable addErrors -ErrorAction SilentlyContinue |
|||
|
|||
@($addErrors) | Should -HaveCount 1 |
|||
$addErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' |
|||
$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/InheritOnly' |
|||
} |
|||
|
|||
It 'Should give each piped descriptor object only its own scope: AppliesTo, then none' { |
|||
$folders = @('AddMixedSDThisFolderOnly', 'AddMixedSDNoScope' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop) |
|||
$entry = @{ Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[1] }) |
|||
) | Add-NTFSAccess -ErrorAction Stop |
|||
|
|||
$scopes = $descriptors | ForEach-Object -Process { Get-TestAccountScope -Acl $_.SecurityDescriptor -Account 'S-1-5-32-546' } |
|||
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None' |
|||
} |
|||
} |
|||
|
|||
Context 'PathSimple parameter set' { |
|||
It 'Should report a missing path''s error by category and target, still scope the next path''s entry, and keep binding -Path through the pipeline once -AppliesTo selects the set' { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('AddSimpleMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimpleNext' |
|||
|
|||
Add-NTFSAccess -Path $missing, $file -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorVariable addErrors -ErrorAction SilentlyContinue |
|||
|
|||
$addErrors | Should -HaveCount 1 |
|||
$addErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$addErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$addErrors[0].TargetObject | Should -Be $missing |
|||
$fileRule = @((Get-Acl -LiteralPath $file).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) |
|||
$fileRule | Should -HaveCount 1 |
|||
|
|||
# -AppliesTo named explicitly selects PathSimple while -Path arrives through the pipeline. |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimplePipedFolder' -Directory |
|||
|
|||
Get-Item2 -Path $folder | Add-NTFSAccess -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop |
|||
|
|||
$guestsRule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-32-546' }) |
|||
$guestsRule | Should -HaveCount 1 |
|||
$guestsRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit) |
|||
$guestsRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) |
|||
$guestsRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeTrue |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Remove-NTFSAccess' { |
|||
Context 'PathComplex parameter set with one denied path and -PassThru' { |
|||
BeforeAll { |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It 'Should write one RemoveAceError for the blocked path, leave it unchanged, and return -PassThru entries only for the path it changed' { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruBlocked' |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruNext' |
|||
Add-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
Block-TestWritePermission -Sandbox $sandbox -Path $blocked |
|||
$before = (Get-Acl -LiteralPath $blocked).Sddl |
|||
|
|||
$result = @(Remove-NTFSAccess -Path $blocked, $next -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$removeErrors | Should -HaveCount 1 |
|||
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' |
|||
$removeErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$removeErrors[0].TargetObject | Should -Be $blocked |
|||
(Get-Acl -LiteralPath $blocked).Sddl | Should -BeExactly $before |
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { |
|||
$_ | Should -BeOfType ([Security2.FileSystemAccessRule2]) |
|||
$_.FullName | Should -Be $next |
|||
} |
|||
@($result | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty |
|||
} |
|||
} |
|||
|
|||
# As for Add-NTFSAccess, the property AppliesTo of the piped object selects PathSimple, and the entry that the cmdlet |
|||
# addresses is the one of the scope it names: the rest of an entry of a wider scope stays. |
|||
Context 'Choosing the parameter set of an object bound through the pipeline by property name' { |
|||
It 'Should leave <Meaning> of an entry that applies to this folder, subfolders, and files when the piped object that has <Shape> removes its rights' -ForEach @( |
|||
@{ Name = 'RemovePipedThisFolderOnly'; Shape = 'AppliesTo ThisFolderOnly'; Extra = @{ AppliesTo = 'ThisFolderOnly' }; Meaning = 'the part that only subfolders and files inherit'; Left = 'ContainerInherit, ObjectInherit/InheritOnly' } |
|||
@{ Name = 'RemovePipedNoScope'; Shape = 'no scope'; Extra = @{}; Meaning = 'nothing'; Left = '' } |
|||
) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
|
|||
[pscustomobject] (@{ Path = $folder; Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } + $Extra) | Remove-NTFSAccess -ErrorAction Stop |
|||
|
|||
Get-TestAccountScope -Acl (Get-Acl -LiteralPath $folder) -Account 'S-1-5-32-546' | Should -BeExactly $Left |
|||
} |
|||
|
|||
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error and change nothing' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipedBoth' -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
$before = (Get-Acl -LiteralPath $folder).Sddl |
|||
$object = [pscustomobject]@{ Path = $folder; Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; InheritanceFlags = 'None' } |
|||
|
|||
{ $object | Remove-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' |
|||
|
|||
(Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before |
|||
} |
|||
} |
|||
|
|||
# As for Add-NTFSAccess: the scope that the cmdlet derives from AppliesTo must not stay for the next object. |
|||
Context 'Objects with different scopes in one pipeline' { |
|||
It 'Should remove from each piped path object the entry of its own scope: AppliesTo, none, AppliesTo, none' { |
|||
$folders = @('RemoveMixedFirst', 'RemoveMixedSecond', 'RemoveMixedThird', 'RemoveMixedFourth' | ForEach-Object -Process { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
$folder |
|||
}) |
|||
$entry = @{ Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ Path = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[1] }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[2]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ Path = $folders[3] }) |
|||
) | Remove-NTFSAccess -ErrorAction Stop |
|||
|
|||
$remaining = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl (Get-Acl -LiteralPath $_) -Account 'S-1-5-32-546' } |
|||
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly||ContainerInherit, ObjectInherit/InheritOnly|' |
|||
} |
|||
|
|||
It 'Should remove from each piped descriptor object the entry of its own scope: AppliesTo, then none' { |
|||
$folders = @('RemoveMixedSDFirst', 'RemoveMixedSDSecond' | ForEach-Object -Process { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
$folder |
|||
}) |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop) |
|||
$entry = @{ Account = 'S-1-5-32-546'; AccessRights = 'ReadData' } |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[1] }) |
|||
) | Remove-NTFSAccess -ErrorAction Stop |
|||
|
|||
$remaining = $descriptors | ForEach-Object -Process { Get-TestAccountScope -Acl $_.SecurityDescriptor -Account 'S-1-5-32-546' } |
|||
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly|' |
|||
} |
|||
} |
|||
|
|||
Context 'PathSimple parameter set' { |
|||
It 'Should take only the requested rights from an entry scoped by -AppliesTo without -RemoveSpecific, keep its flags, and leave another account''s entry untouched' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleScope' -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights Modify -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights Delete -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
|
|||
Remove-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
|
|||
$rules = (Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, $sidType) |
|||
$everyoneRule = @($rules | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) |
|||
$everyoneRule | Should -HaveCount 1 |
|||
$everyoneRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeFalse |
|||
$everyoneRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::WriteData) | Should -BeTrue |
|||
$everyoneRule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit') |
|||
$everyoneRule[0].PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None) |
|||
$guestsRule = @($rules | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-32-546' }) |
|||
$guestsRule | Should -HaveCount 1 |
|||
$guestsRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Delete) | Should -BeTrue |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Clear-NTFSAccess' { |
|||
Context 'Path parameter set on folders bound through the pipeline' { |
|||
It 'Should clear each piped folder''s explicit entries, including an inherit-only one, and leave inherited entries and the owner untouched' { |
|||
$folders = foreach ($name in 'ClearPipeFirst', 'ClearPipeSecond') { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $name -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo SubfoldersAndFilesOnly -ErrorAction Stop |
|||
$folder |
|||
} |
|||
$before = foreach ($folder in $folders) { |
|||
$acl = Get-Acl -LiteralPath $folder |
|||
[pscustomobject]@{ Path = $folder; Owner = $acl.GetOwner($sidType).Value; Inherited = @($acl.GetAccessRules($false, $true, $sidType)).Count } |
|||
} |
|||
foreach ($state in $before) { |
|||
$state.Inherited | Should -BeGreaterThan 0 |
|||
} |
|||
|
|||
Get-Item2 -Path $folders | Clear-NTFSAccess -ErrorAction Stop |
|||
|
|||
foreach ($state in $before) { |
|||
$acl = Get-Acl -LiteralPath $state.Path |
|||
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $state.Inherited |
|||
$acl.AreAccessRulesProtected | Should -BeFalse |
|||
$acl.GetOwner($sidType).Value | Should -Be $state.Owner |
|||
} |
|||
} |
|||
} |
|||
|
|||
Context 'Path parameter set errors' { |
|||
BeforeAll { |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It 'Should report a missing path''s ReadFileError and a blocked path''s ClearAclError, each by category and target' { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('ClearMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
|
|||
Clear-NTFSAccess -Path $missing -ErrorVariable missingErrors -ErrorAction SilentlyContinue |
|||
|
|||
$missingErrors | Should -HaveCount 1 |
|||
$missingErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$missingErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$missingErrors[0].TargetObject | Should -Be $missing |
|||
|
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearBlocked' |
|||
Block-TestWritePermission -Sandbox $sandbox -Path $blocked |
|||
$before = (Get-Acl -LiteralPath $blocked).Sddl |
|||
|
|||
Clear-NTFSAccess -Path $blocked -ErrorVariable blockedErrors -ErrorAction SilentlyContinue |
|||
|
|||
$blockedErrors | Should -HaveCount 1 |
|||
$blockedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ClearAclError,*' |
|||
$blockedErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$blockedErrors[0].TargetObject | Should -Be $blocked |
|||
(Get-Acl -LiteralPath $blocked).Sddl | Should -BeExactly $before |
|||
} |
|||
} |
|||
|
|||
Context 'SD parameter set bound through the pipeline' { |
|||
It 'Should clear each piped descriptor''s explicit entries in memory, leave the items unchanged until Set-NTFSSecurityDescriptor, and keep the owner' { |
|||
$files = foreach ($name in 'ClearSdFirst', 'ClearSdSecond') { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name $name |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
$file |
|||
} |
|||
$before = foreach ($file in $files) { |
|||
$acl = Get-Acl -LiteralPath $file |
|||
[pscustomobject]@{ Path = $file; Owner = $acl.GetOwner($sidType).Value; Inherited = @($acl.GetAccessRules($false, $true, $sidType)).Count; Sddl = $acl.Sddl } |
|||
} |
|||
|
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $files -ErrorAction Stop) |
|||
$descriptors | Clear-NTFSAccess -ErrorAction Stop |
|||
|
|||
for ($index = 0; $index -lt $descriptors.Count; $index++) { |
|||
$sd = $descriptors[$index] |
|||
$state = $before[$index] |
|||
@($sd.SecurityDescriptor.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
@($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $state.Inherited |
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeFalse |
|||
(Get-Acl -LiteralPath $state.Path).Sddl | Should -BeExactly $state.Sddl |
|||
} |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptors -ErrorAction Stop |
|||
|
|||
foreach ($state in $before) { |
|||
$acl = Get-Acl -LiteralPath $state.Path |
|||
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
$acl.GetOwner($sidType).Value | Should -Be $state.Owner |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSAccess' { |
|||
Context 'Path parameter set' { |
|||
It 'Should return the same entries for a folder bound through the pipeline and by position, and report a missing path''s error by category and target' { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessPositionParent' -Directory |
|||
Add-NTFSAccess -Path $parent -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
$child = Join-Path -Path $parent -ChildPath 'Child' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
$expected = @(Get-NTFSAccess -Path $child -ErrorAction Stop | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.IsInherited }) |
|||
|
|||
$pipedResult = @(Get-Item2 -Path $child | Get-NTFSAccess -ErrorAction Stop) |
|||
$positionalFolder = Get-ChildItem -LiteralPath $parent -Directory |
|||
$positionalResult = @(Get-NTFSAccess $positionalFolder -ErrorAction Stop) |
|||
|
|||
($pipedResult | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.IsInherited }) -join ';' | Should -Be ($expected -join ';') |
|||
$pipedResult | ForEach-Object -Process { $_.FullName | Should -Be $child } |
|||
($positionalResult | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.IsInherited }) -join ';' | Should -Be ($expected -join ';') |
|||
$positionalResult | ForEach-Object -Process { $_.FullName | Should -Be $child } |
|||
|
|||
$missing = Join-Path -Path $sandbox -ChildPath ('GetAccessMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
|
|||
Get-NTFSAccess -Path $missing -ErrorVariable getErrors -ErrorAction SilentlyContinue |
|||
|
|||
$getErrors | Should -HaveCount 1 |
|||
$getErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$getErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$getErrors[0].TargetObject | Should -Be $missing |
|||
} |
|||
} |
|||
|
|||
Context 'SD parameter set' { |
|||
It 'Should filter an entry added to a descriptor in memory by -Account, partition it with -ExcludeInherited and -ExcludeExplicit, and read two descriptors bound through the pipeline' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessSdFilter' |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
|
|||
Add-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
|
|||
$bySd = @(Get-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -ErrorAction Stop) |
|||
$byPath = @(Get-NTFSAccess -Path $file -Account 'S-1-1-0' -ErrorAction Stop) |
|||
$bySd | Should -HaveCount 1 |
|||
$bySd[0].IsInherited | Should -BeFalse |
|||
$byPath | Should -BeNullOrEmpty |
|||
$explicitOnly = @(Get-NTFSAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorAction Stop) |
|||
@($explicitOnly | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 |
|||
$inheritedOnly = @(Get-NTFSAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop) |
|||
@($inheritedOnly | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -BeNullOrEmpty |
|||
|
|||
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessSdFilter2' |
|||
$expectedFirst = @(Get-NTFSAccess -Path $file -ErrorAction Stop).Count |
|||
$expectedSecond = @(Get-NTFSAccess -Path $file2 -ErrorAction Stop).Count |
|||
|
|||
$piped = @(Get-NTFSSecurityDescriptor -Path $file, $file2 -ErrorAction Stop | Get-NTFSAccess -ErrorAction Stop) |
|||
|
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $file }) | Should -HaveCount $expectedFirst |
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $file2 }) | Should -HaveCount $expectedSecond |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSOrphanedAccess' { |
|||
Context 'Path parameter set' { |
|||
It 'Should mark an orphaned entry explicit on its own folder and inherited on a child, partitioned by -ExcludeInherited and -ExcludeExplicit' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedInheritableFolder' -Directory |
|||
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9101' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
$child = Join-Path -Path $folder -ChildPath 'Child.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
Set-Content -LiteralPath $child -Value 'Child' |
|||
|
|||
$folderResult = @(Get-NTFSOrphanedAccess -Path $folder -ErrorAction Stop) |
|||
$childResult = @(Get-NTFSOrphanedAccess -Path $child -ErrorAction Stop) |
|||
|
|||
$folderResult | Should -HaveCount 1 |
|||
$folderResult[0].IsInherited | Should -BeFalse |
|||
$childResult | Should -HaveCount 1 |
|||
$childResult[0].IsInherited | Should -BeTrue |
|||
@(Get-NTFSOrphanedAccess -Path $child -ExcludeInherited -ErrorAction Stop) | Should -BeNullOrEmpty |
|||
@(Get-NTFSOrphanedAccess -Path $folder -ExcludeExplicit -ErrorAction Stop) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
It 'Should write a ReadFileError for a missing path, still return the orphaned entry of the next path, and accept both bound through the pipeline' { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('OrphanedMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
$withOrphan = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedNext' |
|||
Add-NTFSAccess -Path $withOrphan -Account 'S-1-5-21-1-2-3-9102' -AccessRights ReadData -ErrorAction Stop |
|||
|
|||
$result = @(Get-NTFSOrphanedAccess -Path $missing, $withOrphan -ErrorVariable orphanErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$orphanErrors | Should -HaveCount 1 |
|||
$orphanErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$orphanErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$orphanErrors[0].TargetObject | Should -Be $missing |
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType ([Security2.FileSystemAccessRule2]) |
|||
$result[0].FullName | Should -Be $withOrphan |
|||
"$($result[0].Account.Sid)" | Should -Be 'S-1-5-21-1-2-3-9102' |
|||
|
|||
$withOrphan2 = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedNext2' |
|||
Add-NTFSAccess -Path $withOrphan2 -Account 'S-1-5-21-1-2-3-9103' -AccessRights ReadData -ErrorAction Stop |
|||
|
|||
$pipedResult = @(Get-Item2 -Path $withOrphan, $withOrphan2 | Get-NTFSOrphanedAccess -ErrorAction Stop) |
|||
|
|||
$pipedResult | Should -HaveCount 2 |
|||
@($pipedResult | Where-Object -FilterScript { $_.FullName -eq $withOrphan }) | Should -HaveCount 1 |
|||
@($pipedResult | Where-Object -FilterScript { $_.FullName -eq $withOrphan2 }) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
|
|||
Context 'SD parameter set' { |
|||
It 'Should filter a descriptor''s orphaned entries by -Account, report both orphaned Sids by default, exclude them all with -ExcludeExplicit, and read two descriptors bound through the pipeline' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSdFile' |
|||
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9201' -AccessRights ReadData -ErrorAction Stop |
|||
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9202' -AccessRights ReadData -ErrorAction Stop |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
|
|||
$filtered = @(Get-NTFSOrphanedAccess -SecurityDescriptor $sd -Account 'S-1-5-21-1-2-3-9202' -ErrorAction Stop) |
|||
$default = @(Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ErrorAction Stop) |
|||
$excludeExplicit = @(Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop) |
|||
|
|||
$filtered | Should -HaveCount 1 |
|||
"$($filtered[0].Account.Sid)" | Should -Be 'S-1-5-21-1-2-3-9202' |
|||
($default | ForEach-Object -Process { "$($_.Account.Sid)" } | Sort-Object) -join ',' | Should -Be (('S-1-5-21-1-2-3-9201', 'S-1-5-21-1-2-3-9202' | Sort-Object) -join ',') |
|||
$excludeExplicit | Should -BeNullOrEmpty |
|||
|
|||
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSdFile2' |
|||
Add-NTFSAccess -Path $file2 -Account 'S-1-5-21-1-2-3-9203' -AccessRights ReadData -ErrorAction Stop |
|||
|
|||
$piped = @(Get-NTFSSecurityDescriptor -Path $file, $file2 -ErrorAction Stop | Get-NTFSOrphanedAccess -ErrorAction Stop) |
|||
|
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $file }) | Should -HaveCount 2 |
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $file2 }) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSSimpleAccess' { |
|||
Context 'Path parameter set, unreadable folder' { |
|||
BeforeAll { |
|||
# Without this, an elevated session auto-enables the Backup privilege in BeginProcessing (every |
|||
# BaseCmdletWithPrivControl cmdlet does, see EnableFileSystemPrivileges) and that alone, with no retry |
|||
# logic in this cmdlet, bypasses the owner-rights read denial below; forcing it off reproduces the same |
|||
# denial as a basic user, like PathErrors.Tests.ps1 does for its own read-denial cases. |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It 'Should report an unreadable folder''s error by category and target and still report the next folder''s entries' { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleBlocked' -Directory |
|||
Block-TestReadPermission -Sandbox $sandbox -Path $blocked |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleNext' -Directory |
|||
Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
|
|||
$result = @(Get-NTFSSimpleAccess -Path $blocked, $next -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$simpleErrors | Should -HaveCount 1 |
|||
$simpleErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*' |
|||
$simpleErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$simpleErrors[0].TargetObject | Should -Be $blocked |
|||
@($result | Where-Object -FilterScript { $_.FullName -eq $blocked }) | Should -BeNullOrEmpty |
|||
@($result | Where-Object -FilterScript { $_.FullName -eq $next }) | Should -Not -BeNullOrEmpty |
|||
} |
|||
} |
|||
|
|||
Context 'SD parameter set' { |
|||
It 'Should filter and partition a descriptor''s entries like the equivalent -Path call, and read two descriptors bound through the pipeline' { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleSdParent' -Directory |
|||
Add-NTFSAccess -Path $parent -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop |
|||
$child = Join-Path -Path $parent -ChildPath 'Child' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
Add-NTFSAccess -Path $child -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
$sd = Get-NTFSSecurityDescriptor -Path $child -ErrorAction Stop |
|||
|
|||
$bySd = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd -ErrorAction Stop) |
|||
$byPath = @(Get-NTFSSimpleAccess -Path $child -IncludeRootFolder:$false -ErrorAction Stop) |
|||
(($bySd | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Identity.Sid, $_.AccessRights, $_.AccessControlType }) | Sort-Object) -join ';' | |
|||
Should -Be ((($byPath | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Identity.Sid, $_.AccessRights, $_.AccessControlType }) | Sort-Object) -join ';') |
|||
|
|||
$filtered = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -ErrorAction Stop) |
|||
$filtered | Should -HaveCount 1 |
|||
$explicitOnly = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorAction Stop) |
|||
$inheritedOnly = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop) |
|||
($explicitOnly.Count + $inheritedOnly.Count) | Should -Be $bySd.Count |
|||
@($explicitOnly | Where-Object -FilterScript { $_.Identity.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1 |
|||
@($inheritedOnly | Where-Object -FilterScript { $_.Identity.Sid -eq 'S-1-5-32-546' }) | Should -HaveCount 1 |
|||
|
|||
$piped = @(Get-NTFSSecurityDescriptor -Path $child, $parent -ErrorAction Stop | Get-NTFSSimpleAccess -ErrorAction Stop) |
|||
|
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $child }) | Should -HaveCount $bySd.Count |
|||
@($piped | Where-Object -FilterScript { $_.FullName -eq $parent }) | Should -Not -BeNullOrEmpty |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSEffectiveAccess' { |
|||
Context 'Path parameter set' { |
|||
It 'Should report an allowed right as present and a denied right as absent for a protected DACL' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveRights' |
|||
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop |
|||
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights ReadData -ErrorAction Stop |
|||
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights WriteData -AccessType Deny -ErrorAction Stop |
|||
|
|||
$result = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop |
|||
|
|||
$result.AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue |
|||
$result.AccessRights.HasFlag([Security2.FileSystemRights2]::WriteData) | Should -BeFalse |
|||
} |
|||
|
|||
It 'Should return one result per item bound through the pipeline, in order' { |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectivePipeFirst' |
|||
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectivePipeSecond' |
|||
|
|||
$result = @(Get-Item2 -Path $first, $second | Get-NTFSEffectiveAccess -Account 'S-1-5-32-546' -WarningAction SilentlyContinue -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
$result[0].FullName | Should -Be $first |
|||
$result[1].FullName | Should -Be $second |
|||
} |
|||
} |
|||
|
|||
Context 'SecurityDescriptor parameter set' { |
|||
It 'Should follow a descriptor changed only in memory while the -Path result stays on disk, and read two descriptors bound through the pipeline' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveSdFile' |
|||
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop |
|||
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights FullControl -ErrorAction Stop |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
|
|||
$pathBefore = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop |
|||
$sdBefore = Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop |
|||
$pathBefore.AccessRights.HasFlag([Security2.FileSystemRights2]::WriteData) | Should -BeTrue |
|||
$sdBefore.AccessRights.HasFlag([Security2.FileSystemRights2]::WriteData) | Should -BeTrue |
|||
|
|||
Add-NTFSAccess -SecurityDescriptor $sd -Account $currentUser -AccessType Deny -AccessRights WriteData -ErrorAction Stop |
|||
|
|||
$sdAfter = Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop |
|||
$pathAfter = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop |
|||
$sdAfter.AccessRights.HasFlag([Security2.FileSystemRights2]::WriteData) | Should -BeFalse |
|||
$pathAfter.AccessRights.HasFlag([Security2.FileSystemRights2]::WriteData) | Should -BeTrue |
|||
|
|||
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveSdFile2' |
|||
Disable-NTFSAccessInheritance -Path $file2 -RemoveInheritedAccessRules -ErrorAction Stop |
|||
Add-NTFSAccess -Path $file2 -Account $currentUser -AccessRights FullControl -ErrorAction Stop |
|||
|
|||
$piped = @(Get-NTFSSecurityDescriptor -Path $file, $file2 -ErrorAction Stop | Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop) |
|||
|
|||
$piped | Should -HaveCount 2 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Declared output types and parameter set boundaries' { |
|||
It '<Name> should declare <Types> as its output type' -ForEach $declaredOutputTypes { |
|||
@((Get-Command -Name $Name).OutputType.Name) | Should -Be $Types |
|||
} |
|||
|
|||
It '<_> should require -AppliesTo only in the Simple parameter sets, -InheritanceFlags/-PropagationFlags only in the Complex parameter sets, and reject combining -AppliesTo with -InheritanceFlags without changing the item' -ForEach @( |
|||
'Add-NTFSAccess', 'Remove-NTFSAccess' |
|||
) { |
|||
$parameterSets = (Get-Command -Name $_).ParameterSets |
|||
|
|||
foreach ($setName in 'PathSimple', 'SDSimple') { |
|||
$parameterSet = $parameterSets | Where-Object -Property Name -EQ -Value $setName |
|||
($parameterSet.Parameters | Where-Object -Property Name -EQ -Value 'AppliesTo').IsMandatory | Should -BeTrue |
|||
$parameterSet.Parameters.Name | Should -Not -Contain 'InheritanceFlags' |
|||
$parameterSet.Parameters.Name | Should -Not -Contain 'PropagationFlags' |
|||
} |
|||
foreach ($setName in 'PathComplex', 'SDComplex') { |
|||
$parameterSet = $parameterSets | Where-Object -Property Name -EQ -Value $setName |
|||
$parameterSet.Parameters.Name | Should -Not -Contain 'AppliesTo' |
|||
$parameterSet.Parameters.Name | Should -Contain 'InheritanceFlags' |
|||
$parameterSet.Parameters.Name | Should -Contain 'PropagationFlags' |
|||
} |
|||
|
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name ('Boundary{0}' -f $_.Replace('-', '')) |
|||
$before = (Get-Acl -LiteralPath $file).Sddl |
|||
|
|||
{ & $_ -Path $file -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -InheritanceFlags None -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'AmbiguousParameterSet*' |
|||
|
|||
(Get-Acl -LiteralPath $file).Sddl | Should -BeExactly $before |
|||
} |
|||
} |
|||
@ -0,0 +1,726 @@ |
|||
<# |
|||
Closes the parameter-set gaps that the judged test matrix (judged-audit.csv / judged-audit-notes.md) found for |
|||
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. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$canReadAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
|||
} |
|||
|
|||
BeforeAll { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' |
|||
Import-Module -Name $modulePath -Force -ErrorAction Stop |
|||
$sandbox = New-TestSandbox -Name 'ParameterSetsAudit' |
|||
Push-Location -LiteralPath $sandbox |
|||
|
|||
$sidType = [System.Security.Principal.SecurityIdentifier] |
|||
$probe = 'S-1-1-0' |
|||
$keeper = 'S-1-5-32-545' |
|||
$entry = @{ Account = $probe; AccessRights = 'ReadData' } |
|||
|
|||
function Get-TestAuditSddl { |
|||
param ([Parameter(Mandatory)] [string] $Path) |
|||
(Get-Acl -LiteralPath $Path -Audit).GetSecurityDescriptorSddlForm('Audit') |
|||
} |
|||
|
|||
function Get-TestDaclSddl { |
|||
param ([Parameter(Mandatory)] [string] $Path) |
|||
(Get-Acl -LiteralPath $Path).Sddl |
|||
} |
|||
|
|||
function Get-TestAuditAcl { |
|||
# The SACL read alone, through .NET. Get-Acl -Audit reads it together with the other sections, and on a domain |
|||
# member that reports the SACL of an item without audit entries as protected from inheritance, which is not its |
|||
# state (see Security2\FileSystem\FileSystemSecurity2.cs). Matches the helper of ParameterSets.Inheritance.Tests.ps1. |
|||
param ([Parameter(Mandatory)] [string] $Path) |
|||
|
|||
$sections = [System.Security.AccessControl.AccessControlSections]::Audit |
|||
$info = if (Test-Path -LiteralPath $Path -PathType Container) { |
|||
New-Object -TypeName 'System.IO.DirectoryInfo' -ArgumentList $Path |
|||
} |
|||
else { |
|||
New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path |
|||
} |
|||
if ($PSVersionTable.PSEdition -eq 'Desktop') { |
|||
$info.GetAccessControl($sections) |
|||
} |
|||
else { |
|||
[System.IO.FileSystemAclExtensions]::GetAccessControl($info, $sections) |
|||
} |
|||
} |
|||
|
|||
function Get-TestAccessOnlyDescriptor { |
|||
# A descriptor read with the Access section only, like one that Get-NTFSSecurityDescriptor returns without |
|||
# the Security privilege: the audit cmdlets have nothing to read or change and report it the same way. |
|||
param ([Parameter(Mandatory)] [string] $Path) |
|||
New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList ( |
|||
(Get-Item2 -Path $Path), [System.Security.AccessControl.AccessControlSections]::Access |
|||
) |
|||
} |
|||
|
|||
function Get-TestAccountAuditRule { |
|||
param ( |
|||
[Parameter(Mandatory)] [System.Security.AccessControl.FileSystemSecurity] $Acl, |
|||
[Parameter(Mandatory)] [string] $Account |
|||
) |
|||
@($Acl.GetAuditRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account }) |
|||
} |
|||
|
|||
function Get-TestAuditScope { |
|||
# The scopes of the explicit audit entries of the probe account, each as 'inheritance flags/propagation flags', joined by ';' |
|||
param ([Parameter(Mandatory)] [System.Security.AccessControl.FileSystemSecurity] $Acl) |
|||
@(Get-TestAccountAuditRule -Acl $Acl -Account $probe | |
|||
ForEach-Object -Process { '{0}/{1}' -f $_.InheritanceFlags, $_.PropagationFlags }) -join ';' |
|||
} |
|||
} |
|||
|
|||
AfterAll { |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'Audit cmdlets with a security descriptor that has no audit section' { |
|||
# TestAuditSection (#109) handles this the same way for every cmdlet that takes -SecurityDescriptor: Audit.Tests.ps1 |
|||
# line 425 already asserts the ErrorId for Add-NTFSAudit, Remove-NTFSAudit, and Clear-NTFSAudit; this data set adds |
|||
# the category and the target, which must be the descriptor itself, not its path, and also covers Get-NTFSAudit and |
|||
# Get-NTFSOrphanedAudit and the Simple parameter sets of Add and Remove. None of this needs the Security privilege: |
|||
# the descriptor already carries no audit section before the cmdlet runs. |
|||
It '<Command> should write a ReadSecurityError with category InvalidData and the descriptor as target for the <Set> set, and change nothing' -ForEach @( |
|||
@{ Command = 'Add-NTFSAudit'; Set = 'SDSimple'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; PassThru = $true } } |
|||
@{ Command = 'Add-NTFSAudit'; Set = 'SDComplex'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; PassThru = $true } } |
|||
@{ Command = 'Remove-NTFSAudit'; Set = 'SDSimple'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; PassThru = $true } } |
|||
@{ Command = 'Remove-NTFSAudit'; Set = 'SDComplex'; Parameters = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; PassThru = $true } } |
|||
@{ Command = 'Clear-NTFSAudit'; Set = 'SD'; Parameters = @{} } |
|||
@{ Command = 'Get-NTFSAudit'; Set = 'SD'; Parameters = @{} } |
|||
@{ Command = 'Get-NTFSOrphanedAudit'; Set = 'SD'; Parameters = @{} } |
|||
) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoAuditSection' |
|||
$sd = Get-TestAccessOnlyDescriptor -Path $file |
|||
|
|||
$result = @(& $Command -SecurityDescriptor $sd @Parameters -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].TargetObject | Should -Be $sd |
|||
$sd.SecurityDescriptor.GetSecurityDescriptorSddlForm('Audit') | Should -BeNullOrEmpty |
|||
} |
|||
} |
|||
|
|||
Describe 'Add-NTFSAudit' { |
|||
Context 'Binding -Path from the pipeline by property name' { |
|||
# The PathSimple row pipes AppliesTo, which selects the set; the PathSimpleNamed row names -AppliesTo instead, and the |
|||
# piped object brings only the path. |
|||
It 'Should add the audit entry for the piped <Set> object that exists, write a ReadFileError for the one that does not, and leave the DACL unchanged' -ForEach @( |
|||
@{ Set = 'PathComplex'; ExtraProperties = @{ InheritanceFlags = 'None'; PropagationFlags = 'None' }; Named = @{} } |
|||
@{ Set = 'PathSimple'; ExtraProperties = @{ AppliesTo = 'ThisFolderOnly' }; Named = @{} } |
|||
@{ Set = 'PathSimpleNamed'; ExtraProperties = @{}; Named = @{ AppliesTo = 'ThisFolderOnly' } } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('AddPipeMissing-{0}' -f [guid]::NewGuid().ToString('N')) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
$good = New-TestSandboxItem -Sandbox $sandbox -Name "AddPipeGood$Set" -Directory |
|||
$daclBefore = Get-TestDaclSddl -Path $good |
|||
|
|||
$items = @( |
|||
[pscustomobject] (@{ FullName = $missing; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
[pscustomobject] (@{ FullName = $good; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
) |
|||
|
|||
$result = @($items | Add-NTFSAudit @Named -PassThru -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$auditErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$auditErrors[0].TargetObject | Should -Be $missing |
|||
$result | Should -HaveCount 1 |
|||
$result[0].FullName | Should -Be $good |
|||
# The default scope of a new entry is ContainerInherit and ObjectInherit; None shows that the scope was honored |
|||
$rule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $good -Audit) -Account $probe |
|||
$rule | Should -HaveCount 1 |
|||
$rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None) |
|||
Get-TestDaclSddl -Path $good | Should -BeExactly $daclBefore |
|||
} |
|||
} |
|||
|
|||
Context 'Binding -SecurityDescriptor from the pipeline by property name' { |
|||
It 'Should add the audit entry for the piped <Set> object that has an audit section, write a ReadSecurityError for the one that does not, and leave the item on disk unchanged until Set-NTFSSecurityDescriptor writes it' -ForEach @( |
|||
@{ Set = 'SDSimple'; ExtraProperties = @{ AppliesTo = 'ThisFolderOnly' } } |
|||
@{ Set = 'SDComplex'; ExtraProperties = @{ InheritanceFlags = 'None'; PropagationFlags = 'None' } } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$badFile = New-TestSandboxItem -Sandbox $sandbox -Name "AddPipeSDBad$Set" |
|||
$sdBad = Get-TestAccessOnlyDescriptor -Path $badFile |
|||
# A folder, so that the scope of the entry is observable; the piped AppliesTo property binds SDSimple, as it binds |
|||
# PathSimple for a path |
|||
$goodFile = New-TestSandboxItem -Sandbox $sandbox -Name "AddPipeSDGood$Set" -Directory |
|||
$auditBefore = Get-TestAuditSddl -Path $goodFile |
|||
$daclBefore = Get-TestDaclSddl -Path $goodFile |
|||
$sdGood = Get-NTFSSecurityDescriptor -Path $goodFile -ErrorAction Stop |
|||
|
|||
$items = @( |
|||
[pscustomobject] (@{ SecurityDescriptor = $sdBad; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
[pscustomobject] (@{ SecurityDescriptor = $sdGood; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
) |
|||
|
|||
$result = @($items | Add-NTFSAudit -PassThru -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$auditErrors[0].TargetObject | Should -Be $sdBad |
|||
$result | Should -HaveCount 1 |
|||
$result[0].FullName | Should -Be $goodFile |
|||
$rule = Get-TestAccountAuditRule -Acl $sdGood.SecurityDescriptor -Account $probe |
|||
$rule | Should -HaveCount 1 |
|||
$rule[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None) |
|||
Get-TestAuditSddl -Path $goodFile | Should -BeExactly $auditBefore |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sdGood -ErrorAction Stop |
|||
|
|||
(Get-TestAccountAuditRule -Acl (Get-TestAuditAcl -Path $goodFile) -Account $probe) | Should -HaveCount 1 |
|||
Get-TestAuditSddl -Path $goodFile | Should -Not -BeExactly $auditBefore |
|||
Get-TestDaclSddl -Path $goodFile | Should -BeExactly $daclBefore |
|||
} |
|||
} |
|||
|
|||
# The property AppliesTo selects PathSimple, InheritanceFlags and PropagationFlags select PathComplex, and an object that |
|||
# has none of them gets the default scope, as for Add-NTFSAccess (ParameterSets.Access.Tests.ps1). |
|||
Context 'Choosing the parameter set of an object bound through the pipeline by property name' { |
|||
It 'Should give the audit entry for the piped object that has <Shape> the scope <Scope>' -ForEach @( |
|||
@{ Name = 'AddPipedAuditThisFolderOnly'; Shape = 'AppliesTo ThisFolderOnly'; Extra = @{ AppliesTo = 'ThisFolderOnly' }; Scope = 'None/None' } |
|||
@{ Name = 'AddPipedAuditSubfolders'; Shape = 'AppliesTo ThisFolderAndSubfolders'; Extra = @{ AppliesTo = 'ThisFolderAndSubfolders' }; Scope = 'ContainerInherit/None' } |
|||
@{ Name = 'AddPipedAuditFlags'; Shape = 'InheritanceFlags and PropagationFlags'; Extra = @{ InheritanceFlags = 'ContainerInherit'; PropagationFlags = 'InheritOnly' }; Scope = 'ContainerInherit/InheritOnly' } |
|||
@{ Name = 'AddPipedAuditNoScope'; Shape = 'no scope'; Extra = @{}; Scope = 'ContainerInherit, ObjectInherit/None' } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory |
|||
|
|||
[pscustomobject] (@{ FullName = $folder; Account = $probe; AccessRights = 'ReadData' } + $Extra) | Add-NTFSAudit -ErrorAction Stop |
|||
|
|||
Get-TestAuditScope -Acl (Get-Acl -LiteralPath $folder -Audit) | Should -BeExactly $Scope |
|||
} |
|||
|
|||
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipedAuditBoth' -Directory |
|||
$object = [pscustomobject]@{ FullName = $folder; Account = $probe; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; InheritanceFlags = 'None' } |
|||
|
|||
{ $object | Add-NTFSAudit -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' |
|||
} |
|||
} |
|||
|
|||
# PowerShell resets only the parameters that the previous object bound from the pipeline. The scope that the cmdlet |
|||
# derives from AppliesTo must therefore not stay in the fields behind InheritanceFlags and PropagationFlags: the next |
|||
# object that has no scope, or only one flag, would get it (ParameterSets.Access.Tests.ps1 has the same rows). |
|||
Context 'Objects with different scopes in one pipeline' { |
|||
It 'Should give each piped path object only its own scope: AppliesTo, none, AppliesTo, one flag, none' -Skip:(-not $canReadAudit) { |
|||
$folders = @('AddMixedAuditThisFolderOnly', 'AddMixedAuditNoScope', 'AddMixedAuditInheritOnly', 'AddMixedAuditOneFlag', 'AddMixedAuditNoScopeAgain' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ FullName = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[1] }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[2]; AppliesTo = 'SubfoldersAndFilesOnly' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[3]; InheritanceFlags = 'ContainerInherit' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[4] }) |
|||
) | Add-NTFSAudit -ErrorAction Stop |
|||
|
|||
$scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } |
|||
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly|ContainerInherit/None|ContainerInherit, ObjectInherit/None' |
|||
} |
|||
|
|||
It 'Should give each row of a CSV file that Import-Csv reads and that has an AppliesTo column its own scope' -Skip:(-not $canReadAudit) { |
|||
$folders = @('AddCsvAuditThisFolderOnly', 'AddCsvAuditSubfolders', 'AddCsvAuditInheritOnly' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvAuditScopes.csv' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv |
|||
Set-Content -LiteralPath $csv -Value @( |
|||
'FullName,Account,AccessRights,AppliesTo' |
|||
('"{0}",{1},ReadData,ThisFolderOnly' -f $folders[0], $probe) |
|||
('"{0}",{1},ReadData,ThisFolderAndSubfolders' -f $folders[1], $probe) |
|||
('"{0}",{1},ReadData,SubfoldersAndFilesOnly' -f $folders[2], $probe) |
|||
) |
|||
|
|||
Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorAction Stop |
|||
|
|||
$scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } |
|||
$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) { |
|||
$folders = @('AddCsvAuditBlankFirst', 'AddCsvAuditBlankCell', 'AddCsvAuditBlankLast' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvAuditBlank.csv' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv |
|||
Set-Content -LiteralPath $csv -Value @( |
|||
'FullName,Account,AccessRights,AppliesTo' |
|||
('"{0}",{1},ReadData,ThisFolderOnly' -f $folders[0], $probe) |
|||
('"{0}",{1},ReadData,' -f $folders[1], $probe) |
|||
('"{0}",{1},ReadData,SubfoldersAndFilesOnly' -f $folders[2], $probe) |
|||
) |
|||
|
|||
Import-Csv -LiteralPath $csv | Add-NTFSAudit -ErrorAction Stop |
|||
|
|||
$scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } |
|||
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly' |
|||
} |
|||
|
|||
# $null cannot be bound at all: the object fails, loudly, and the cmdlet goes on with the next one. |
|||
It 'Should report a binding error for the piped object whose AppliesTo is $null, leave its folder alone, and still process the other objects' -Skip:(-not $canReadAudit) { |
|||
$folders = @('AddNullAuditFirst', 'AddNullAuditMiddle', 'AddNullAuditLast' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ FullName = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[1]; AppliesTo = $null }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[2]; AppliesTo = 'SubfoldersAndFilesOnly' }) |
|||
) | Add-NTFSAudit -ErrorVariable auditErrors -ErrorAction SilentlyContinue |
|||
|
|||
@($auditErrors) | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*' |
|||
$scopes = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } |
|||
$scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly' |
|||
} |
|||
|
|||
It 'Should give each piped descriptor object only its own scope: AppliesTo, then none' -Skip:(-not $canReadAudit) { |
|||
$folders = @('AddMixedAuditSDThisFolderOnly', 'AddMixedAuditSDNoScope' | |
|||
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory }) |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop) |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[1] }) |
|||
) | Add-NTFSAudit -ErrorAction Stop |
|||
|
|||
$scopes = $descriptors | ForEach-Object -Process { Get-TestAuditScope -Acl $_.SecurityDescriptor } |
|||
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None' |
|||
} |
|||
} |
|||
|
|||
Context 'Conflicting parameters' { |
|||
It 'Should reject -AppliesTo combined with -InheritanceFlags with an AmbiguousParameterSet error' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddAmbiguous' |
|||
|
|||
{ Add-NTFSAudit -Path $file -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -InheritanceFlags None -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'AmbiguousParameterSet,NTFSSecurity.AddAudit' |
|||
} |
|||
} |
|||
|
|||
Context 'Direct call of -SecurityDescriptor with -AppliesTo' { |
|||
# Add-NTFSAudit SDSimple (judged-audit.csv): every existing call binds it only through a splat |
|||
# (PermissionScopes.Tests.ps1) or through position (Audit.Tests.ps1); this one names the set's parameters |
|||
# directly. |
|||
It 'Should add the entry to the descriptor when -SecurityDescriptor, -Account, -AccessRights, and -AppliesTo are all named directly' -Skip:(-not $canReadAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddDirectSDSimple' |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
|
|||
$result = @(Add-NTFSAudit -SecurityDescriptor $sd -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType [Security2.FileSystemAuditRule2] |
|||
(Get-TestAccountAuditRule -Acl $sd.SecurityDescriptor -Account $probe) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Remove-NTFSAudit' { |
|||
Context 'Binding -Path from the pipeline by property name' { |
|||
# The PathSimple row pipes AppliesTo and the PathSimpleNamed row names it. Neither can tell the sets apart here: the |
|||
# entry is on a file, where the scope does not matter. The Contexts on folders below do. |
|||
It "Should remove the audit entry for the piped <Set> object that exists, write a ReadFileError for the one that does not, and keep the other account's entry" -ForEach @( |
|||
@{ Set = 'PathComplex'; ExtraProperties = @{ InheritanceFlags = 'None'; PropagationFlags = 'None' }; Named = @{} } |
|||
@{ Set = 'PathSimple'; ExtraProperties = @{ AppliesTo = 'ThisFolderOnly' }; Named = @{} } |
|||
@{ Set = 'PathSimpleNamed'; ExtraProperties = @{}; Named = @{ AppliesTo = 'ThisFolderOnly' } } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('RemovePipeMissing-{0}' -f [guid]::NewGuid().ToString('N')) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
$good = New-TestSandboxItem -Sandbox $sandbox -Name "RemovePipeGood$Set" |
|||
Add-NTFSAudit -Path $good @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
Add-NTFSAudit -Path $good -Account $keeper -AccessRights Delete -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$daclBefore = Get-TestDaclSddl -Path $good |
|||
|
|||
$items = @( |
|||
[pscustomobject] (@{ FullName = $missing; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
[pscustomobject] (@{ FullName = $good; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
) |
|||
|
|||
$result = @($items | Remove-NTFSAudit @Named -PassThru -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$auditErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$auditErrors[0].TargetObject | Should -Be $missing |
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { $_.FullName | Should -Be $good } |
|||
(Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $good -Audit) -Account $probe) | Should -BeNullOrEmpty |
|||
(Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $good -Audit) -Account $keeper) | Should -HaveCount 1 |
|||
Get-TestDaclSddl -Path $good | Should -BeExactly $daclBefore |
|||
} |
|||
} |
|||
|
|||
Context 'Binding -SecurityDescriptor from the pipeline by property name' { |
|||
It "Should remove the audit entry for the piped <Set> object that has an audit section, write a ReadSecurityError for the one that does not, and keep the other account's entry" -ForEach @( |
|||
@{ Set = 'SDSimple'; ExtraProperties = @{ AppliesTo = 'ThisFolderOnly' } } |
|||
@{ Set = 'SDComplex'; ExtraProperties = @{ InheritanceFlags = 'None'; PropagationFlags = 'None' } } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$badFile = New-TestSandboxItem -Sandbox $sandbox -Name "RemovePipeSDBad$Set" |
|||
$sdBad = Get-TestAccessOnlyDescriptor -Path $badFile |
|||
$goodFile = New-TestSandboxItem -Sandbox $sandbox -Name "RemovePipeSDGood$Set" |
|||
Add-NTFSAudit -Path $goodFile @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
Add-NTFSAudit -Path $goodFile -Account $keeper -AccessRights Delete -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$sdGood = Get-NTFSSecurityDescriptor -Path $goodFile -ErrorAction Stop |
|||
|
|||
$items = @( |
|||
[pscustomobject] (@{ SecurityDescriptor = $sdBad; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
[pscustomobject] (@{ SecurityDescriptor = $sdGood; Account = $probe; AccessRights = 'ReadData' } + $ExtraProperties) |
|||
) |
|||
|
|||
$result = @($items | Remove-NTFSAudit -PassThru -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$auditErrors[0].TargetObject | Should -Be $sdBad |
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { $_.FullName | Should -Be $goodFile } |
|||
(Get-TestAccountAuditRule -Acl $sdGood.SecurityDescriptor -Account $probe) | Should -BeNullOrEmpty |
|||
(Get-TestAccountAuditRule -Acl $sdGood.SecurityDescriptor -Account $keeper) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
|
|||
# As for Add-NTFSAudit, the property AppliesTo of the piped object selects PathSimple, and the entry that the cmdlet |
|||
# addresses is the one of the scope it names: the rest of an entry of a wider scope stays. |
|||
Context 'Choosing the parameter set of an object bound through the pipeline by property name' { |
|||
It 'Should leave <Meaning> of an audit entry that applies to this folder, subfolders, and files when the piped object that has <Shape> removes its rights' -ForEach @( |
|||
@{ Name = 'RemovePipedAuditThisFolderOnly'; Shape = 'AppliesTo ThisFolderOnly'; Extra = @{ AppliesTo = 'ThisFolderOnly' }; Meaning = 'the part that only subfolders and files inherit'; Left = 'ContainerInherit, ObjectInherit/InheritOnly' } |
|||
@{ Name = 'RemovePipedAuditNoScope'; Shape = 'no scope'; Extra = @{}; Meaning = 'nothing'; Left = '' } |
|||
) -Skip:(-not $canReadAudit) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory |
|||
Add-NTFSAudit -Path $folder @entry -ErrorAction Stop |
|||
|
|||
[pscustomobject] (@{ FullName = $folder; Account = $probe; AccessRights = 'ReadData' } + $Extra) | Remove-NTFSAudit -ErrorAction Stop |
|||
|
|||
Get-TestAuditScope -Acl (Get-Acl -LiteralPath $folder -Audit) | Should -BeExactly $Left |
|||
} |
|||
|
|||
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error' { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipedAuditBoth' -Directory |
|||
$object = [pscustomobject]@{ FullName = $folder; Account = $probe; AccessRights = 'ReadData'; AppliesTo = 'ThisFolderOnly'; InheritanceFlags = 'None' } |
|||
|
|||
{ $object | Remove-NTFSAudit -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*' |
|||
} |
|||
} |
|||
|
|||
# As for Add-NTFSAudit: the scope that the cmdlet derives from AppliesTo must not stay for the next object. |
|||
Context 'Objects with different scopes in one pipeline' { |
|||
It 'Should remove from each piped path object the entry of its own scope: AppliesTo, none, AppliesTo, none' -Skip:(-not $canReadAudit) { |
|||
$folders = @('RemoveMixedAuditFirst', 'RemoveMixedAuditSecond', 'RemoveMixedAuditThird', 'RemoveMixedAuditFourth' | ForEach-Object -Process { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory |
|||
Add-NTFSAudit -Path $folder @entry -ErrorAction Stop |
|||
$folder |
|||
}) |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ FullName = $folders[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[1] }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[2]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ FullName = $folders[3] }) |
|||
) | Remove-NTFSAudit -ErrorAction Stop |
|||
|
|||
$remaining = $folders | ForEach-Object -Process { Get-TestAuditScope -Acl (Get-Acl -LiteralPath $_ -Audit) } |
|||
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly||ContainerInherit, ObjectInherit/InheritOnly|' |
|||
} |
|||
|
|||
It 'Should remove from each piped descriptor object the entry of its own scope: AppliesTo, then none' -Skip:(-not $canReadAudit) { |
|||
$folders = @('RemoveMixedAuditSDFirst', 'RemoveMixedAuditSDSecond' | ForEach-Object -Process { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory |
|||
Add-NTFSAudit -Path $folder @entry -ErrorAction Stop |
|||
$folder |
|||
}) |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop) |
|||
|
|||
@( |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[0]; AppliesTo = 'ThisFolderOnly' }) |
|||
[pscustomobject] ($entry + @{ SecurityDescriptor = $descriptors[1] }) |
|||
) | Remove-NTFSAudit -ErrorAction Stop |
|||
|
|||
$remaining = $descriptors | ForEach-Object -Process { Get-TestAuditScope -Acl $_.SecurityDescriptor } |
|||
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly|' |
|||
} |
|||
} |
|||
|
|||
Context 'Without -RemoveSpecific' { |
|||
# Judged-audit-notes.md: "the Modify entry keeps the other rights (probe first, then pin)". |
|||
It 'Should narrow an entry that grants more rights than it removes, instead of deleting it' -Skip:(-not $canReadAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveNarrow' |
|||
Add-NTFSAudit -Path $file -Account $probe -AccessRights Modify -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
Add-NTFSAudit -Path $file -Account $keeper -AccessRights Delete -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
|
|||
Remove-NTFSAudit -Path $file @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
|
|||
$probeRule = Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $file -Audit) -Account $probe |
|||
$probeRule | Should -HaveCount 1 |
|||
$probeRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeFalse |
|||
$probeRule[0].FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Delete) | Should -BeTrue |
|||
(Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $file -Audit) -Account $keeper) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
|
|||
Context 'Direct call of -Path with -AppliesTo' { |
|||
# Remove-NTFSAudit PathSimple (judged-audit.csv): every existing call binds it only through a splat |
|||
# (PermissionScopes.Tests.ps1); this one names the set's parameters directly. |
|||
It 'Should remove the entry from the path when -Path, -Account, -AccessRights, and -AppliesTo are all named directly' -Skip:(-not $canReadAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveDirectPathSimple' |
|||
Add-NTFSAudit -Path $file -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
Add-NTFSAudit -Path $file -Account $keeper -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
|
|||
$result = @(Remove-NTFSAudit -Path $file -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAuditRule2] } |
|||
(Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $file -Audit) -Account $probe) | Should -BeNullOrEmpty |
|||
(Get-TestAccountAuditRule -Acl (Get-Acl -LiteralPath $file -Audit) -Account $keeper) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
|
|||
Context 'Direct call of -SecurityDescriptor with -AppliesTo' { |
|||
# Remove-NTFSAudit SDSimple (judged-audit.csv): every existing call binds it only through a splat |
|||
# (PermissionScopes.Tests.ps1); this one names the set's parameters directly. |
|||
It 'Should remove the entry from the descriptor when -SecurityDescriptor, -Account, -AccessRights, and -AppliesTo are all named directly' -Skip:(-not $canReadAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveDirectSDSimple' |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
Add-NTFSAudit -SecurityDescriptor $sd -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
Add-NTFSAudit -SecurityDescriptor $sd -Account $keeper -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
|
|||
$result = @(Remove-NTFSAudit -SecurityDescriptor $sd -Account $probe -AccessRights ReadData -AppliesTo ThisFolderOnly -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -Not -BeNullOrEmpty |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType [Security2.FileSystemAuditRule2] } |
|||
(Get-TestAccountAuditRule -Acl $sd.SecurityDescriptor -Account $probe) | Should -BeNullOrEmpty |
|||
(Get-TestAccountAuditRule -Acl $sd.SecurityDescriptor -Account $keeper) | Should -HaveCount 1 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Clear-NTFSAudit' { |
|||
Context 'Taking paths from the pipeline' { |
|||
It 'Should clear the audit entries of each path taken from the pipeline and leave the DACL of each unchanged' -Skip:(-not $canReadAudit) { |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearPipeFirst' |
|||
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearPipeSecond' |
|||
foreach ($path in $first, $second) { |
|||
Add-NTFSAudit -Path $path @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
} |
|||
$daclBefore = @((Get-TestDaclSddl -Path $first), (Get-TestDaclSddl -Path $second)) |
|||
|
|||
$first, $second | Clear-NTFSAudit -ErrorVariable clearErrors -ErrorAction SilentlyContinue |
|||
|
|||
$clearErrors | Should -BeNullOrEmpty |
|||
@(Get-NTFSAudit -Path $first -ExcludeInherited) | Should -BeNullOrEmpty |
|||
@(Get-NTFSAudit -Path $second -ExcludeInherited) | Should -BeNullOrEmpty |
|||
Get-TestDaclSddl -Path $first | Should -BeExactly $daclBefore[0] |
|||
Get-TestDaclSddl -Path $second | Should -BeExactly $daclBefore[1] |
|||
} |
|||
} |
|||
|
|||
Context 'With -DisableInheritance' { |
|||
# judged-audit.csv: "-DisableInheritance on a path is passed (Audit.Tests.ps1:629) but its end state |
|||
# (protected SACL, no copied inherited entries) is never asserted." |
|||
It "Should protect the SACL and not copy the parent's inherited entry when clearing a child path" -Skip:(-not $canReadAudit) { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearDisableParent' -Directory |
|||
Add-NTFSAudit -Path $parent -Account $keeper -AccessRights Delete -AppliesTo ThisFolderAndFiles -AuditFlags Success -ErrorAction Stop |
|||
$child = Join-Path -Path $parent -ChildPath 'Child.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
Set-Content -LiteralPath $child -Value 'Child' |
|||
Add-NTFSAudit -Path $child @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
# Precondition: the parent's rule really is inherited onto the child before the test clears it. |
|||
@(Get-NTFSAudit -Path $child -ExcludeExplicit) | Should -HaveCount 1 |
|||
$daclBefore = Get-TestDaclSddl -Path $child |
|||
|
|||
Clear-NTFSAudit -Path $child -DisableInheritance -ErrorVariable clearErrors -ErrorAction SilentlyContinue |
|||
|
|||
$clearErrors | Should -BeNullOrEmpty |
|||
(Get-TestAuditAcl -Path $child).AreAuditRulesProtected | Should -BeTrue |
|||
@((Get-Acl -LiteralPath $child -Audit).GetAuditRules($true, $true, $sidType)) | Should -BeNullOrEmpty |
|||
Get-TestDaclSddl -Path $child | Should -BeExactly $daclBefore |
|||
} |
|||
} |
|||
|
|||
Context 'Binding -SecurityDescriptor from the pipeline' { |
|||
It 'Should clear each piped descriptor that has an audit section and continue past one that does not' -Skip:(-not $canReadAudit) { |
|||
$badFile = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearPipeSDBad' |
|||
$sdBad = Get-TestAccessOnlyDescriptor -Path $badFile |
|||
$goodFile = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearPipeSDGood' |
|||
Add-NTFSAudit -Path $goodFile @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$sdGood = Get-NTFSSecurityDescriptor -Path $goodFile -ErrorAction Stop |
|||
$auditBefore = Get-TestAuditSddl -Path $goodFile |
|||
$daclBefore = Get-TestDaclSddl -Path $goodFile |
|||
|
|||
$sdBad, $sdGood | Clear-NTFSAudit -ErrorVariable clearErrors -ErrorAction SilentlyContinue |
|||
|
|||
$clearErrors | Should -HaveCount 1 |
|||
$clearErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$clearErrors[0].TargetObject | Should -Be $sdBad |
|||
@($sdGood.SecurityDescriptor.GetAuditRules($true, $false, $sidType)) | Should -BeNullOrEmpty |
|||
Get-TestAuditSddl -Path $goodFile | Should -BeExactly $auditBefore |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sdGood -ErrorAction Stop |
|||
|
|||
@(Get-NTFSAudit -Path $goodFile -ExcludeInherited) | Should -BeNullOrEmpty |
|||
Get-TestDaclSddl -Path $goodFile | Should -BeExactly $daclBefore |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSAudit' { |
|||
Context 'With several paths' { |
|||
# judged-audit.csv observation: Audit.Tests.ps1:70-73 no longer reproduces a failing path followed by one |
|||
# that succeeds (the deny entry blocks the DACL, not the SACL that the cmdlet reads). A missing path does. |
|||
It 'Should return the entries of the existing path in either position and a ReadFileError, category OpenError, for the missing one' -Skip:(-not $canReadAudit) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditFolder' -Directory |
|||
Add-NTFSAudit -Path $folder -Account $probe -AccessRights ReadData -AuditFlags Success -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('GetAuditMissing-{0}' -f [guid]::NewGuid().ToString('N')) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
|
|||
$forward = @(Get-NTFSAudit -Path $missing, $folder -ErrorVariable forwardErrors -ErrorAction SilentlyContinue) |
|||
$backward = @(Get-NTFSAudit -Path $folder, $missing -ErrorVariable backwardErrors -ErrorAction SilentlyContinue) |
|||
|
|||
foreach ($errors in $forwardErrors, $backwardErrors) { |
|||
$errors | Should -HaveCount 1 |
|||
$errors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' |
|||
$errors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$errors[0].TargetObject | Should -Be $missing |
|||
} |
|||
foreach ($result in $forward, $backward) { |
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType [Security2.FileSystemAuditRule2] |
|||
$result[0].FullName | Should -Be $folder |
|||
$result[0].Account.Sid | Should -Be $probe |
|||
$result[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue |
|||
$result[0].AuditFlags | Should -Be ([System.Security.AccessControl.AuditFlags]::Success) |
|||
$result[0].IsInherited | Should -BeFalse |
|||
$result[0].InheritanceEnabled | Should -Be (-not (Get-Acl -LiteralPath $folder -Audit).AreAuditRulesProtected) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Context 'Taking paths from the pipeline' { |
|||
It 'Should return the entries of each path object taken from the pipeline by value' -Skip:(-not $canReadAudit) { |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditPipeFirst' |
|||
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditPipeSecond' |
|||
foreach ($path in $first, $second) { |
|||
Add-NTFSAudit -Path $path @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
} |
|||
|
|||
$result = @(Get-Item2 -Path $first, $second | Get-NTFSAudit -ErrorAction Stop) |
|||
|
|||
($result.FullName | Sort-Object) -join '|' | Should -Be (($first, $second | Sort-Object) -join '|') |
|||
} |
|||
} |
|||
|
|||
Context 'When the audit entries cannot be read' { |
|||
# judged-audit.csv: Audit.Tests.ps1:48 asserts the ErrorId only; this adds the category and the target. |
|||
It 'Should write a ReadSecurityError with category OpenError and the path as target without the Security privilege' -Skip:$canReadAudit { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditNoPrivilege' |
|||
|
|||
$entries = @(Get-NTFSAudit -Path $file -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$entries | Should -BeNullOrEmpty |
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$auditErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$auditErrors[0].TargetObject | Should -Be $file |
|||
} |
|||
} |
|||
|
|||
Context 'Binding -SecurityDescriptor from the pipeline' { |
|||
It 'Should return the entries of the piped descriptor that has an audit section and continue past one that does not' -Skip:(-not $canReadAudit) { |
|||
$badFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditSDBad' |
|||
$sdBad = Get-TestAccessOnlyDescriptor -Path $badFile |
|||
$goodFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAuditSDGood' |
|||
Add-NTFSAudit -Path $goodFile @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$expected = @(Get-NTFSAudit -Path $goodFile -ErrorAction Stop | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.AuditFlags }) |
|||
$sdGood = Get-NTFSSecurityDescriptor -Path $goodFile -ErrorAction Stop |
|||
|
|||
$result = @($sdBad, $sdGood | Get-NTFSAudit -ErrorVariable auditErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$auditErrors | Should -HaveCount 1 |
|||
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidData' |
|||
$auditErrors[0].TargetObject | Should -Be $sdBad |
|||
$result | Should -Not -BeNullOrEmpty |
|||
($result | ForEach-Object -Process { '{0}|{1}|{2}' -f $_.Account.Sid, $_.AccessRights, $_.AuditFlags }) -join ';' | Should -Be ($expected -join ';') |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSOrphanedAudit' { |
|||
Context 'Taking paths from the pipeline' { |
|||
It 'Should return the orphaned entries of each path taken from the pipeline and write a ReadError, category OpenError, for one that does not exist' -Skip:(-not $canReadAudit) { |
|||
$orphaned = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedPipe' |
|||
Add-NTFSAudit -Path $orphaned -Account 'S-1-5-21-1-2-3-9001' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$normal = New-TestSandboxItem -Sandbox $sandbox -Name 'NormalPipe' |
|||
Add-NTFSAudit -Path $normal @entry -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('OrphanedMissing-{0}' -f [guid]::NewGuid().ToString('N')) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
|
|||
$result = @($missing, $orphaned, $normal | Get-NTFSOrphanedAudit -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$orphanedErrors | Should -HaveCount 1 |
|||
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*' |
|||
$orphanedErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$orphanedErrors[0].TargetObject | Should -Be $missing |
|||
$result | Should -HaveCount 1 |
|||
$result[0].FullName | Should -Be $orphaned |
|||
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-9001' |
|||
} |
|||
} |
|||
|
|||
Context 'Binding -SecurityDescriptor from the pipeline' { |
|||
It 'Should return the orphaned entries of the piped descriptor that has an audit section and continue past one that does not' -Skip:(-not $canReadAudit) { |
|||
$badFile = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSDBad' |
|||
$sdBad = Get-TestAccessOnlyDescriptor -Path $badFile |
|||
$orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSDGood' |
|||
Add-NTFSAudit -Path $orphanedFile -Account 'S-1-5-21-1-2-3-9002' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$sdOrphaned = Get-NTFSSecurityDescriptor -Path $orphanedFile -ErrorAction Stop |
|||
|
|||
$result = @($sdBad, $sdOrphaned | Get-NTFSOrphanedAudit -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$orphanedErrors | Should -HaveCount 1 |
|||
$orphanedErrors[0].TargetObject | Should -Be $sdBad |
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType [Security2.FileSystemAuditRule2] |
|||
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-9002' |
|||
$result[0].FullName | Should -Be $orphanedFile |
|||
} |
|||
} |
|||
|
|||
Context 'Direct call of -SecurityDescriptor' { |
|||
# Get-NTFSOrphanedAudit SD (judged-audit.csv): both existing calls bind it only because PowerShell resolves |
|||
# a piped object with a SecurityDescriptor property to this parameter (Audit.Tests.ps1:243, :294); this one |
|||
# names it directly. |
|||
It 'Should return the orphaned entries when -SecurityDescriptor is named directly' -Skip:(-not $canReadAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedDirectSD' |
|||
Add-NTFSAudit -Path $file -Account 'S-1-5-21-1-2-3-9003' -AccessRights ReadData -InheritanceFlags None -PropagationFlags None -ErrorAction Stop |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop |
|||
|
|||
$result = @(Get-NTFSOrphanedAudit -SecurityDescriptor $sd -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType [Security2.FileSystemAuditRule2] |
|||
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-9003' |
|||
$result[0].FullName | Should -Be $file |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,751 @@ |
|||
<# |
|||
Closes TestNeeded gaps of the judged parameter-set matrix (judged-inheritance.csv / judged-inheritance-notes.md, |
|||
scanned at HEAD 6696f68) for the inheritance, owner, and security-descriptor cmdlets: Disable/Enable-NTFSAccessInheritance, |
|||
Disable/Enable-NTFSAuditInheritance, Get/Set-NTFSInheritance, Get/Set-NTFSOwner, Get-NTFSSecurityDescriptor. The module |
|||
built in NTFSSecurity\bin\Release runs on files and folders in a sandbox folder. Every assertion reads state with .NET |
|||
(Get-Acl, Get-Acl -Audit) or compares an independent oracle, asserts an ErrorId/category/target triple, exercises a |
|||
pipeline that no existing test used, or checks continuation to the next item/descriptor; none of it repeats what |
|||
Inheritance.Tests.ps1, Owner.Tests.ps1, SecurityDescriptor.Tests.ps1, SecurityDescriptorSets.Tests.ps1, or |
|||
PathErrors.Tests.ps1 already assert. Tests that change or read the audit section need the Security privilege and skip |
|||
without it; CI runs them elevated too. This file does not test the 17 shared owner-restore handlers against what a |
|||
later command raises there (an open maintainer decision, see systemPatterns.md), the abandoned PrivilegeEnabler, or |
|||
any Decision 22 behavior, and it does not add a new gap test for Set-NTFSSecurityDescriptor, whose own row is judged |
|||
GapKind None. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$canChangeAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
|||
$holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' |
|||
$canBypassDeny = Test-PrivilegeHeld -Name 'SeBackupPrivilege' |
|||
|
|||
# State: file/folder x Enable(Disable=false/Enable=true) x Remove, read back with Get-Acl, not with Get-NTFSAccess. |
|||
$accessOracleCases = @(foreach ($type in 'file', 'folder') { |
|||
foreach ($enable in $false, $true) { |
|||
foreach ($remove in $false, $true) { |
|||
@{ Type = $type; Enable = $enable; Remove = $remove } |
|||
} |
|||
} |
|||
}) |
|||
|
|||
# State: the same transitions for the SACL, read back with Get-Acl -Audit, not with Get-NTFSAudit/Get-NTFSInheritance. |
|||
$auditOracleCases = @(foreach ($type in 'file', 'folder') { |
|||
foreach ($enable in $false, $true) { |
|||
foreach ($remove in $false, $true) { |
|||
@{ Type = $type; Enable = $enable; Remove = $remove } |
|||
} |
|||
} |
|||
}) |
|||
|
|||
# State: the -RemoveInheritedAccessRules/-RemoveExplicitAccessRules switches with a -SecurityDescriptor, which |
|||
# SecurityDescriptorSets.Tests.ps1 covers only without the switch (Disable) or without any explicit entry (Enable). |
|||
$sdAccessSwitchCases = @( |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; SwitchName = 'RemoveInheritedAccessRules'; Remove = $true } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; SwitchName = 'RemoveExplicitAccessRules'; Remove = $false } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; SwitchName = 'RemoveExplicitAccessRules'; Remove = $true } |
|||
) |
|||
|
|||
# Error: a fully denied write with -PassThru, asserting the category and the next item's new state together (S1). |
|||
$deniedWriteCases = @( |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ModifySdError'; PreDisable = $false; ExpectedFlag = $false } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ModifySdError'; PreDisable = $true; ExpectedFlag = $true } |
|||
@{ Command = 'Set-NTFSInheritance'; Parameters = @{ AccessInheritanceEnabled = $false }; ErrorId = 'ModifySdError'; PreDisable = $false; ExpectedFlag = $false } |
|||
) |
|||
|
|||
# Error: RestoreOwnerError of these three Path cmdlets, none of which has a test for it today (S3). |
|||
$restoreOwnerCases = @( |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; Parameters = @{}; PreDisable = $false; ExpectedProtected = $true } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; Parameters = @{}; PreDisable = $true; ExpectedProtected = $false } |
|||
@{ Command = 'Set-NTFSInheritance'; Parameters = @{ AccessInheritanceEnabled = $false }; PreDisable = $false; ExpectedProtected = $true } |
|||
) |
|||
|
|||
# Error: a missing path, asserting the OpenError category and exactly one object for the next path (S1 + S6). |
|||
$missingPathOpenErrorCases = @( |
|||
@{ Command = 'Get-NTFSInheritance'; ErrorId = 'ReadFileError' } |
|||
@{ Command = 'Get-NTFSSecurityDescriptor'; ErrorId = 'ReadFileError' } |
|||
) |
|||
|
|||
# Error: Set-NTFSOwner id/category/target together, for a missing path and for a refused owner (S1 + S6). |
|||
$setOwnerErrorCases = @( |
|||
@{ Scenario = 'Missing'; ErrorId = 'ReadFileError'; Category = 'OpenError' } |
|||
@{ Scenario = 'Refused'; ErrorId = 'SetOwnerError'; Category = 'WriteError' } |
|||
) |
|||
|
|||
# Pipeline: Get-ChildItem2 piped into the four Path-set inheritance cmdlets with -PassThru, access and audit. |
|||
$pipedInheritanceCases = @( |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; Scope = 'access'; PreDisable = $false; Expected = $false } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; Scope = 'access'; PreDisable = $true; Expected = $true } |
|||
@{ Command = 'Disable-NTFSAuditInheritance'; Scope = 'audit'; PreDisable = $false; Expected = $false } |
|||
@{ Command = 'Enable-NTFSAuditInheritance'; Scope = 'audit'; PreDisable = $true; Expected = $true } |
|||
) |
|||
|
|||
# Pipeline: two security descriptors piped into the four SecurityDescriptor-set inheritance cmdlets. |
|||
$pipedInheritanceSdCases = @( |
|||
@{ Command = 'Disable-NTFSAccessInheritance'; Scope = 'access'; PreDisable = $false; Expected = $false } |
|||
@{ Command = 'Enable-NTFSAccessInheritance'; Scope = 'access'; PreDisable = $true; Expected = $true } |
|||
@{ Command = 'Disable-NTFSAuditInheritance'; Scope = 'audit'; PreDisable = $false; Expected = $false } |
|||
@{ Command = 'Enable-NTFSAuditInheritance'; Scope = 'audit'; PreDisable = $true; Expected = $true } |
|||
) |
|||
} |
|||
|
|||
BeforeAll { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' |
|||
Import-Module -Name $modulePath -Force -ErrorAction Stop |
|||
$sandbox = New-TestSandbox -Name 'ParameterSetsInheritance' |
|||
Push-Location -LiteralPath $sandbox |
|||
|
|||
$sidType = [System.Security.Principal.SecurityIdentifier] |
|||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value |
|||
# An owner that the user can assign only with the Restore privilege |
|||
$trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464' |
|||
$privateData = (Get-Module -Name NTFSSecurity).PrivateData |
|||
$enablePrivileges = $privateData['EnablePrivileges'] |
|||
# Recomputed here (not only in BeforeDiscovery): an It body runs in the Run phase, which does not see |
|||
# BeforeDiscovery variables, and the two piped-inheritance tests need this flag inside the body, not only in -Skip. |
|||
$canChangeAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
|||
$holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege' |
|||
|
|||
function Get-TestAcl { |
|||
# .NET, because Get-Acl of an elevated Windows PowerShell reads also items that deny reading their permissions. |
|||
# .NET Core has the method as an extension method. Matches the helper of PathErrors.Tests.ps1. |
|||
param ([string] $Path) |
|||
|
|||
$info = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path |
|||
if ($PSVersionTable.PSEdition -eq 'Desktop') { |
|||
$info.GetAccessControl() |
|||
} |
|||
else { |
|||
[System.IO.FileSystemAclExtensions]::GetAccessControl($info) |
|||
} |
|||
} |
|||
|
|||
function Get-TestAuditAcl { |
|||
# The SACL read alone, through .NET. Get-Acl -Audit reads it together with the other sections, and on a domain |
|||
# member that reports the SACL of an item without audit entries as protected from inheritance, which is not its |
|||
# state (see Security2\FileSystem\FileSystemSecurity2.cs). Matches the helper of ParameterSets.Audit.Tests.ps1. |
|||
param ([string] $Path) |
|||
|
|||
$sections = [System.Security.AccessControl.AccessControlSections]::Audit |
|||
$info = if (Test-Path -LiteralPath $Path -PathType Container) { |
|||
New-Object -TypeName 'System.IO.DirectoryInfo' -ArgumentList $Path |
|||
} |
|||
else { |
|||
New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path |
|||
} |
|||
if ($PSVersionTable.PSEdition -eq 'Desktop') { |
|||
$info.GetAccessControl($sections) |
|||
} |
|||
else { |
|||
[System.IO.FileSystemAclExtensions]::GetAccessControl($info, $sections) |
|||
} |
|||
} |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'State: independent .NET oracles for inheritance transitions and descriptor-only changes' { |
|||
It 'Should set the DACL protected flag to <Enable> and the explicit/inherited rule counts for -Remove=<Remove> on a <Type>, read with Get-Acl' -ForEach $accessOracleCases { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessOracleParent' -Directory |
|||
$path = Join-Path -Path $parent -ChildPath 'Child' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $path |
|||
$parentAccount = 'S-1-5-21-1-2-3-4921' |
|||
$childAccount = 'S-1-5-21-1-2-3-4922' |
|||
Add-NTFSAccess -Path $parent -Account $parentAccount -AccessRights ReadData -ErrorAction Stop |
|||
if ($Type -eq 'folder') { New-Item -ItemType Directory -Path $path | Out-Null } else { Set-Content -LiteralPath $path -Value 'Child' } |
|||
Add-NTFSAccess -Path $path -Account $childAccount -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
$inheritedBefore = @((Get-Acl -LiteralPath $path).GetAccessRules($false, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $parentAccount }) |
|||
$inheritedBefore | Should -HaveCount 1 |
|||
if ($Enable) { |
|||
Disable-NTFSAccessInheritance -Path $path -RemoveInheritedAccessRules -ErrorAction Stop |
|||
$parameters = @{ RemoveExplicitAccessRules = $Remove } |
|||
$command = 'Enable-NTFSAccessInheritance' |
|||
} |
|||
else { |
|||
$parameters = @{ RemoveInheritedAccessRules = $Remove } |
|||
$command = 'Disable-NTFSAccessInheritance' |
|||
} |
|||
|
|||
& $command -Path $path @parameters -ErrorAction Stop |
|||
|
|||
$acl = Get-Acl -LiteralPath $path |
|||
$acl.AreAccessRulesProtected | Should -Be (-not $Enable) |
|||
$parentRules = @($acl.GetAccessRules($true, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $parentAccount }) |
|||
if ($Enable) { |
|||
$parentRules | Should -HaveCount 1 |
|||
$parentRules[0].IsInherited | Should -BeTrue |
|||
} |
|||
elseif ($Remove) { |
|||
$parentRules | Should -BeNullOrEmpty |
|||
} |
|||
else { |
|||
$parentRules | Should -HaveCount 1 |
|||
$parentRules[0].IsInherited | Should -BeFalse |
|||
} |
|||
$childRules = @($acl.GetAccessRules($true, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $childAccount }) |
|||
if ($Enable -and $Remove) { $childRules | Should -BeNullOrEmpty } else { $childRules | Should -HaveCount 1 } |
|||
} |
|||
|
|||
It 'Should set the SACL protected flag to <Enable> and the explicit/inherited audit-rule counts for -Remove=<Remove> on a <Type>, read with Get-Acl -Audit' -Skip:(-not $canChangeAudit) -ForEach $auditOracleCases { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditOracleParent' -Directory |
|||
$path = Join-Path -Path $parent -ChildPath 'Child' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $path |
|||
$parentAccount = 'S-1-5-21-1-2-3-4931' |
|||
$childAccount = 'S-1-5-21-1-2-3-4932' |
|||
Add-NTFSAudit -Path $parent -Account $parentAccount -AccessRights ReadData -AuditFlags Success -ErrorAction Stop |
|||
if ($Type -eq 'folder') { New-Item -ItemType Directory -Path $path | Out-Null } else { Set-Content -LiteralPath $path -Value 'Child' } |
|||
Add-NTFSAudit -Path $path -Account $childAccount -AccessRights Delete -AuditFlags Failure -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
$inheritedBefore = @((Get-Acl -LiteralPath $path -Audit).GetAuditRules($false, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $parentAccount }) |
|||
$inheritedBefore | Should -HaveCount 1 |
|||
$daclBefore = (Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access') |
|||
if ($Enable) { |
|||
Disable-NTFSAuditInheritance -Path $path -RemoveInheritedAuditRules -ErrorAction Stop |
|||
$parameters = @{ RemoveExplicitAuditRules = $Remove } |
|||
$command = 'Enable-NTFSAuditInheritance' |
|||
} |
|||
else { |
|||
$parameters = @{ RemoveInheritedAuditRules = $Remove } |
|||
$command = 'Disable-NTFSAuditInheritance' |
|||
} |
|||
|
|||
& $command -Path $path @parameters -ErrorAction Stop |
|||
|
|||
$acl = Get-Acl -LiteralPath $path -Audit |
|||
$acl.AreAuditRulesProtected | Should -Be (-not $Enable) |
|||
$parentRules = @($acl.GetAuditRules($true, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $parentAccount }) |
|||
if ($Enable) { |
|||
$parentRules | Should -HaveCount 1 |
|||
$parentRules[0].IsInherited | Should -BeTrue |
|||
} |
|||
elseif ($Remove) { |
|||
$parentRules | Should -BeNullOrEmpty |
|||
} |
|||
else { |
|||
$parentRules | Should -HaveCount 1 |
|||
$parentRules[0].IsInherited | Should -BeFalse |
|||
} |
|||
$childRules = @($acl.GetAuditRules($true, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $childAccount }) |
|||
if ($Enable -and $Remove) { $childRules | Should -BeNullOrEmpty } else { $childRules | Should -HaveCount 1 } |
|||
(Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $daclBefore |
|||
} |
|||
|
|||
It '<Command> -SecurityDescriptor should change the in-memory access entries per -<SwitchName>:<Remove>, applied to the item only after Set-NTFSSecurityDescriptor writes it' -ForEach $sdAccessSwitchCases { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SdSwitchParent' -Directory |
|||
$file = Join-Path -Path $parent -ChildPath 'Child' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $file |
|||
Add-NTFSAccess -Path $parent -Account 'S-1-5-21-1-2-3-4941' -AccessRights ReadData -ErrorAction Stop |
|||
Set-Content -LiteralPath $file -Value 'Child' |
|||
if ($Command -eq 'Enable-NTFSAccessInheritance') { |
|||
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-4942' -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop |
|||
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop |
|||
} |
|||
$before = (Get-Acl -LiteralPath $file).Sddl |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
$switchParameters = @{ $SwitchName = $Remove } |
|||
|
|||
& $Command -SecurityDescriptor $sd @switchParameters -ErrorAction Stop |
|||
|
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -Be ($Command -eq 'Disable-NTFSAccessInheritance') |
|||
(Get-Acl -LiteralPath $file).Sddl | Should -BeExactly $before |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
$acl = Get-Acl -LiteralPath $file |
|||
if ($Command -eq 'Disable-NTFSAccessInheritance') { |
|||
$acl.AreAccessRulesProtected | Should -BeTrue |
|||
$inherited = @($acl.GetAccessRules($false, $true, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-21-1-2-3-4941' }) |
|||
$explicitCopy = @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-21-1-2-3-4941' }) |
|||
if ($Remove) { |
|||
$inherited | Should -BeNullOrEmpty |
|||
$explicitCopy | Should -BeNullOrEmpty |
|||
} |
|||
else { |
|||
$explicitCopy | Should -HaveCount 1 |
|||
} |
|||
} |
|||
else { |
|||
$acl.AreAccessRulesProtected | Should -BeFalse |
|||
$explicitChild = @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-5-21-1-2-3-4942' }) |
|||
if ($Remove) { $explicitChild | Should -BeNullOrEmpty } else { $explicitChild | Should -HaveCount 1 } |
|||
} |
|||
} |
|||
|
|||
It 'Set-NTFSInheritance -SecurityDescriptor should set both inheritance flags in one call, changing only the in-memory descriptor until it is written' -Skip:(-not $canChangeAudit) { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorBothFlags' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $file |
|||
$daclBefore = (Get-Acl -LiteralPath $file).Sddl |
|||
$sd = Get-NTFSSecurityDescriptor -Path $file |
|||
|
|||
$result = @(Set-NTFSInheritance -SecurityDescriptor $sd -AccessInheritanceEnabled $false -AuditInheritanceEnabled $false -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0].AccessInheritanceEnabled | Should -BeFalse |
|||
$result[0].AuditInheritanceEnabled | Should -BeFalse |
|||
$sd.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue |
|||
$sd.SecurityDescriptor.AreAuditRulesProtected | Should -BeTrue |
|||
(Get-Acl -LiteralPath $file).Sddl | Should -BeExactly $daclBefore |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop |
|||
|
|||
(Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -BeTrue |
|||
(Get-TestAuditAcl -Path $file).AreAuditRulesProtected | Should -BeTrue |
|||
} |
|||
|
|||
It 'Set-NTFSOwner should change only the owner and leave the Access entries and the group unchanged' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OwnerOnlyChange' |
|||
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop |
|||
$before = Get-Acl -LiteralPath $file |
|||
if ($before.GetOwner($sidType).Value -eq $currentUser) { |
|||
Set-ItResult -Skipped -Because 'the user already owns new items, so Set-NTFSOwner would not change the owner' |
|||
return |
|||
} |
|||
$accessBefore = $before.GetSecurityDescriptorSddlForm('Access') |
|||
$groupBefore = $before.GetGroup($sidType).Value |
|||
|
|||
$result = @(Set-NTFSOwner -Path $file -Account $currentUser -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType ([Security2.FileSystemOwner]) |
|||
$result[0].Owner.Sid | Should -Be $currentUser |
|||
$after = Get-Acl -LiteralPath $file |
|||
$after.GetOwner($sidType).Value | Should -Be $currentUser |
|||
$after.GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $accessBefore |
|||
$after.GetGroup($sidType).Value | Should -Be $groupBefore |
|||
} |
|||
} |
|||
|
|||
Describe 'Errors: categories, targets, and continuation of the inheritance and owner cmdlets' { |
|||
Context 'A denied write, with and without a restorable owner' { |
|||
BeforeAll { |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It '<Command> should report <ErrorId> with category WriteError and target for the denied item, and still return the next item''s new state with -PassThru' -ForEach $deniedWriteCases { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'DeniedWriteBlocked' |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'DeniedWriteNext' |
|||
if ($PreDisable) { |
|||
Disable-NTFSAccessInheritance -Path $blocked, $next -ErrorAction Stop |
|||
} |
|||
Block-TestWritePermission -Sandbox $sandbox -Path $blocked |
|||
$before = (Get-Acl -LiteralPath $blocked).Sddl |
|||
|
|||
$result = @(& $Command -Path $blocked, $next @Parameters -PassThru -ErrorVariable changeErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$changeErrors | Should -HaveCount 1 |
|||
$changeErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$changeErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$changeErrors[0].TargetObject | Should -Be $blocked |
|||
(Get-Acl -LiteralPath $blocked).Sddl | Should -BeExactly $before |
|||
$result | Should -HaveCount 1 |
|||
$result[0].FullName | Should -Be $next |
|||
$result[0].AccessInheritanceEnabled | Should -Be $ExpectedFlag |
|||
} |
|||
|
|||
It '<Command> should report RestoreOwnerError after a successful ownership retry and continue with the next path' -Skip:(-not $holdsRestorePrivilege) -ForEach $restoreOwnerCases { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'RestoreOwnerBlocked' |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'RestoreOwnerNext' |
|||
if ($PreDisable) { |
|||
Disable-NTFSAccessInheritance -Path $blocked, $next -ErrorAction Stop |
|||
} |
|||
$user = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value |
|||
Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @{ $user = 'ChangePermissions' } |
|||
Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $trustedInstaller |
|||
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState | Should -Be 'Disabled' |
|||
|
|||
& $Command -Path $blocked, $next @Parameters -ErrorVariable changeErrors -ErrorAction SilentlyContinue |
|||
|
|||
$changeErrors | Should -HaveCount 1 |
|||
$changeErrors[0].FullyQualifiedErrorId | Should -BeLike 'RestoreOwnerError,*' |
|||
$changeErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$changeErrors[0].TargetObject | Should -Be $blocked |
|||
(Get-TestAcl -Path $blocked).GetOwner($sidType).Value | Should -Be $user |
|||
(Get-TestAcl -Path $blocked).AreAccessRulesProtected | Should -Be $ExpectedProtected |
|||
(Get-TestAcl -Path $next).AreAccessRulesProtected | Should -Be $ExpectedProtected |
|||
} |
|||
} |
|||
|
|||
It '<Command> should write <ErrorId> with category OpenError for a path that does not exist, and return exactly one object for the next path' -ForEach $missingPathOpenErrorCases { |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'NextAfterMissing' |
|||
|
|||
$output = @(& $Command -Path $missing, $next -ErrorVariable pathErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$pathErrors | Should -HaveCount 1 |
|||
$pathErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$pathErrors[0].CategoryInfo.Category | Should -Be 'OpenError' |
|||
$pathErrors[0].TargetObject | Should -Be $missing |
|||
$output | Should -HaveCount 1 |
|||
$output[0].FullName | Should -Be $next |
|||
} |
|||
|
|||
It 'Get-NTFSOwner should write a ReadSecurityError with its id, category, and target together for a denied item, and return exactly one object for the next path' -Skip:$canBypassDeny { |
|||
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'OwnerDenied' |
|||
Block-TestReadPermission -Sandbox $sandbox -Path $blocked |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'OwnerDeniedNext' |
|||
|
|||
$output = @(Get-NTFSOwner -Path $blocked, $next -ErrorVariable ownerErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$ownerErrors | Should -HaveCount 1 |
|||
$ownerErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' |
|||
$ownerErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied' |
|||
$ownerErrors[0].TargetObject | Should -Be $blocked |
|||
$output | Should -HaveCount 1 |
|||
$output[0].FullName | Should -Be $next |
|||
} |
|||
|
|||
Context 'Set-NTFSOwner when Windows refuses the new owner' { |
|||
BeforeAll { |
|||
$privateData['EnablePrivileges'] = $false |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $enablePrivileges |
|||
} |
|||
|
|||
It 'Should write <ErrorId> with its category and target for <Scenario>, and keep processing the remaining path' -ForEach $setOwnerErrorCases { |
|||
if ($Scenario -eq 'Missing') { |
|||
# -Account is one shared value for every -Path of the call, so only a missing path can fail while a |
|||
# second, existing path still succeeds with the very same account. |
|||
$target = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $target |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerNext' |
|||
|
|||
$result = @(Set-NTFSOwner -Path $target, $next -Account $currentUser -PassThru -ErrorVariable ownerErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$ownerErrors | Should -HaveCount 1 |
|||
$ownerErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$ownerErrors[0].CategoryInfo.Category | Should -Be $Category |
|||
$ownerErrors[0].TargetObject | Should -Be $target |
|||
$result | Should -HaveCount 1 |
|||
$result[0].FullName | Should -Be $next |
|||
$result[0].Owner.Sid | Should -Be $currentUser |
|||
} |
|||
else { |
|||
# Windows refuses the same account for every path of the call, so both paths fail; each must still be |
|||
# reported on its own, which proves the loop did not stop after the first one. |
|||
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerRefused' |
|||
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerRefusedNext' |
|||
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState | Should -Not -Be 'Enabled' |
|||
|
|||
$result = @(Set-NTFSOwner -Path $target, $next -Account $trustedInstaller -PassThru -ErrorVariable ownerErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$ownerErrors | Should -HaveCount 2 |
|||
$ownerErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$ownerErrors[0].CategoryInfo.Category | Should -Be $Category |
|||
$ownerErrors[0].TargetObject | Should -Be $target |
|||
$ownerErrors[1].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" |
|||
$ownerErrors[1].CategoryInfo.Category | Should -Be $Category |
|||
$ownerErrors[1].TargetObject | Should -Be $next |
|||
$result | Should -BeNullOrEmpty |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Pipeline: binding into the -Path and -SecurityDescriptor sets that no existing test piped' { |
|||
It '<Command> should take piped folders from Get-ChildItem2 and report the <Scope> inheritance state of each with -PassThru' -ForEach $pipedInheritanceCases { |
|||
if ($Scope -eq 'audit' -and -not $canChangeAudit) { |
|||
Set-ItResult -Skipped -Because 'changing audit inheritance needs the Security privilege' |
|||
return |
|||
} |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedInheritanceParent' -Directory |
|||
$children = foreach ($i in 1, 2) { |
|||
$child = Join-Path -Path $parent -ChildPath "Child$i" |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
$child |
|||
} |
|||
if ($PreDisable) { |
|||
foreach ($child in $children) { |
|||
if ($Scope -eq 'access') { Disable-NTFSAccessInheritance -Path $child -ErrorAction Stop } |
|||
else { Disable-NTFSAuditInheritance -Path $child -ErrorAction Stop } |
|||
} |
|||
} |
|||
|
|||
$result = @(Get-ChildItem2 -Path $parent -Directory | & $Command -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType ([Security2.FileSystemInheritanceInfo]) } |
|||
(@($result.FullName) | Sort-Object) | Should -Be (@($children) | Sort-Object) |
|||
foreach ($child in $children) { |
|||
$item = $result | Where-Object -FilterScript { $_.FullName -eq $child } |
|||
$item | Should -HaveCount 1 |
|||
if ($Scope -eq 'access') { |
|||
$item.AccessInheritanceEnabled | Should -Be $Expected |
|||
(Get-Acl -LiteralPath $child).AreAccessRulesProtected | Should -Be (-not $Expected) |
|||
} |
|||
else { |
|||
$item.AuditInheritanceEnabled | Should -Be $Expected |
|||
(Get-TestAuditAcl -Path $child).AreAuditRulesProtected | Should -Be (-not $Expected) |
|||
} |
|||
} |
|||
} |
|||
|
|||
It '<Command> -SecurityDescriptor should take two piped descriptors and report both in order, written only by Set-NTFSSecurityDescriptor' -ForEach $pipedInheritanceSdCases { |
|||
if ($Scope -eq 'audit' -and -not $canChangeAudit) { |
|||
Set-ItResult -Skipped -Because 'changing audit inheritance needs the Security privilege' |
|||
return |
|||
} |
|||
$files = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "PipedSdInheritance$_" } |
|||
if ($PreDisable) { |
|||
foreach ($file in $files) { |
|||
if ($Scope -eq 'access') { Disable-NTFSAccessInheritance -Path $file -ErrorAction Stop } |
|||
else { Disable-NTFSAuditInheritance -Path $file -ErrorAction Stop } |
|||
} |
|||
} |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $files) |
|||
$descriptors | Should -HaveCount 2 |
|||
|
|||
$result = @($descriptors | & $Command -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
($result.FullName -join '|') | Should -Be ($files -join '|') |
|||
for ($i = 0; $i -lt 2; $i++) { |
|||
if ($Scope -eq 'access') { |
|||
$descriptors[$i].SecurityDescriptor.AreAccessRulesProtected | Should -Be (-not $Expected) |
|||
(Get-Acl -LiteralPath $files[$i]).AreAccessRulesProtected | Should -Be $PreDisable |
|||
} |
|||
else { |
|||
$descriptors[$i].SecurityDescriptor.AreAuditRulesProtected | Should -Be (-not $Expected) |
|||
(Get-TestAuditAcl -Path $files[$i]).AreAuditRulesProtected | Should -Be $PreDisable |
|||
} |
|||
} |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptors -ErrorAction Stop |
|||
|
|||
foreach ($file in $files) { |
|||
if ($Scope -eq 'access') { (Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -Be (-not $Expected) } |
|||
else { (Get-TestAuditAcl -Path $file).AreAuditRulesProtected | Should -Be (-not $Expected) } |
|||
} |
|||
|
|||
# A direct array binds both descriptors within one ProcessRecord call, unlike the pipeline above, which |
|||
# rebinds the parameter once per item; only this direct form can catch a handler that always changes the |
|||
# first descriptor of the set. |
|||
$directFiles = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "DirectSdInheritance$_" } |
|||
if ($PreDisable) { |
|||
foreach ($file in $directFiles) { |
|||
if ($Scope -eq 'access') { Disable-NTFSAccessInheritance -Path $file -ErrorAction Stop } |
|||
else { Disable-NTFSAuditInheritance -Path $file -ErrorAction Stop } |
|||
} |
|||
} |
|||
$directDescriptors = @(Get-NTFSSecurityDescriptor -Path $directFiles) |
|||
|
|||
& $Command -SecurityDescriptor $directDescriptors -ErrorAction Stop |
|||
|
|||
for ($i = 0; $i -lt 2; $i++) { |
|||
if ($Scope -eq 'access') { $directDescriptors[$i].SecurityDescriptor.AreAccessRulesProtected | Should -Be (-not $Expected) } |
|||
else { $directDescriptors[$i].SecurityDescriptor.AreAuditRulesProtected | Should -Be (-not $Expected) } |
|||
} |
|||
} |
|||
|
|||
It 'Get-NTFSInheritance should take piped folder objects from Get-ChildItem2 and a piped custom object by its FullName property' { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritancePipeParent' -Directory |
|||
$children = foreach ($i in 1, 2) { |
|||
$child = Join-Path -Path $parent -ChildPath "Child$i" |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
$child |
|||
} |
|||
|
|||
$result = @(Get-ChildItem2 -Path $parent -Directory | Get-NTFSInheritance -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
(@($result.FullName) | Sort-Object) | Should -Be (@($children) | Sort-Object) |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType ([Security2.FileSystemInheritanceInfo]) } |
|||
|
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritancePipeByProperty' |
|||
$expectedFlag = -not (Get-Acl -LiteralPath $file).AreAccessRulesProtected |
|||
|
|||
$byProperty = @([pscustomobject]@{ FullName = $file } | Get-NTFSInheritance -ErrorAction Stop) |
|||
|
|||
$byProperty | Should -HaveCount 1 |
|||
$byProperty[0].FullName | Should -Be $file |
|||
$byProperty[0].AccessInheritanceEnabled | Should -Be $expectedFlag |
|||
} |
|||
|
|||
It 'Get-NTFSInheritance -SecurityDescriptor should take two piped descriptors, one protected, and report both in order' { |
|||
$protectedItem = New-TestSandboxItem -Sandbox $sandbox -Name 'SdInheritanceProtected' |
|||
Disable-NTFSAccessInheritance -Path $protectedItem -ErrorAction Stop |
|||
$plainItem = New-TestSandboxItem -Sandbox $sandbox -Name 'SdInheritancePlain' |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $protectedItem, $plainItem) |
|||
|
|||
$result = @($descriptors | Get-NTFSInheritance -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
$result[0].FullName | Should -Be $protectedItem |
|||
$result[0].Name | Should -Be ([IO.Path]::GetFileName($protectedItem)) |
|||
$result[0].AccessInheritanceEnabled | Should -BeFalse |
|||
$result[1].FullName | Should -Be $plainItem |
|||
$result[1].AccessInheritanceEnabled | Should -BeTrue |
|||
|
|||
# The same array passed directly to the parameter, not only through the pipeline. |
|||
$byParameter = @(Get-NTFSInheritance -SecurityDescriptor $descriptors -ErrorAction Stop) |
|||
|
|||
$byParameter | Should -HaveCount 2 |
|||
$byParameter[0].AccessInheritanceEnabled | Should -BeFalse |
|||
$byParameter[1].AccessInheritanceEnabled | Should -BeTrue |
|||
} |
|||
|
|||
It 'Set-NTFSInheritance should restore a saved inheritance state through the pipeline by property name' { |
|||
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RoundTripState' -Directory |
|||
$state = Get-NTFSInheritance -Path $path -ErrorAction Stop |
|||
Disable-NTFSAccessInheritance -Path $path -ErrorAction Stop |
|||
(Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeTrue |
|||
|
|||
$result = @($state | Set-NTFSInheritance -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType ([Security2.FileSystemInheritanceInfo]) |
|||
$result[0].FullName | Should -Be $path |
|||
(Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeFalse |
|||
} |
|||
|
|||
It 'Set-NTFSInheritance -SecurityDescriptor should protect both descriptors in memory, written only by Set-NTFSSecurityDescriptor, piped and passed directly to the parameter' { |
|||
$files = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "SdInheritanceSet$_" } |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $files) |
|||
|
|||
$result = @($descriptors | Set-NTFSInheritance -AccessInheritanceEnabled $false -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
($result.FullName -join '|') | Should -Be ($files -join '|') |
|||
$descriptors | ForEach-Object -Process { $_.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue } |
|||
foreach ($file in $files) { (Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -BeFalse } |
|||
|
|||
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptors -ErrorAction Stop |
|||
|
|||
foreach ($file in $files) { (Get-Acl -LiteralPath $file).AreAccessRulesProtected | Should -BeTrue } |
|||
|
|||
# A direct array binds both descriptors within one ProcessRecord call, unlike the pipeline above, which |
|||
# rebinds the parameter once per item; only this direct form can catch a handler that always changes the |
|||
# first descriptor of the set. |
|||
$directFiles = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "DirectSdInheritanceSet$_" } |
|||
$directDescriptors = @(Get-NTFSSecurityDescriptor -Path $directFiles) |
|||
|
|||
Set-NTFSInheritance -SecurityDescriptor $directDescriptors -AccessInheritanceEnabled $false -ErrorAction Stop |
|||
|
|||
$directDescriptors | ForEach-Object -Process { $_.SecurityDescriptor.AreAccessRulesProtected | Should -BeTrue } |
|||
} |
|||
|
|||
It 'Get-NTFSOwner should take two piped folders from Get-ChildItem2 and return the owner of each, verified with Get-Acl' { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'OwnerPipeParent' -Directory |
|||
$children = foreach ($i in 1, 2) { |
|||
$child = Join-Path -Path $parent -ChildPath "Child$i" |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
$child |
|||
} |
|||
|
|||
$result = @(Get-ChildItem2 -Path $parent -Directory | Get-NTFSOwner -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType ([Security2.FileSystemOwner]) } |
|||
foreach ($child in $children) { |
|||
$owner = $result | Where-Object -FilterScript { $_.FullName -eq $child } |
|||
$owner | Should -HaveCount 1 |
|||
$owner.Owner.Sid | Should -Be (Get-Acl -LiteralPath $child).GetOwner($sidType).Value |
|||
} |
|||
} |
|||
|
|||
It 'Get-NTFSOwner -SecurityDescriptor should return the owner of each descriptor, piped and passed directly to the parameter, verified with Get-Acl' { |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdOwnerFirst' |
|||
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdOwnerSecond' |
|||
if ($holdsRestorePrivilege) { |
|||
Set-TestOwner -Sandbox $sandbox -Path $second -Sid $trustedInstaller |
|||
} |
|||
$expectedFirst = (Get-Acl -LiteralPath $first).GetOwner($sidType).Value |
|||
$expectedSecond = (Get-Acl -LiteralPath $second).GetOwner($sidType).Value |
|||
if ($holdsRestorePrivilege) { |
|||
$expectedFirst | Should -Not -Be $expectedSecond |
|||
} |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $first, $second) |
|||
|
|||
$piped = @($descriptors | Get-NTFSOwner -ErrorAction Stop) |
|||
|
|||
$piped | Should -HaveCount 2 |
|||
$piped | ForEach-Object -Process { $_ | Should -BeOfType ([Security2.FileSystemOwner]) } |
|||
$piped[0].FullName | Should -Be $first |
|||
$piped[0].Owner.Sid | Should -Be $expectedFirst |
|||
$piped[1].FullName | Should -Be $second |
|||
$piped[1].Owner.Sid | Should -Be $expectedSecond |
|||
|
|||
# Row 14 of the matrix was bound only by a call the scan could not classify; call it directly and by name too. |
|||
$direct = @(Get-NTFSOwner -SecurityDescriptor $descriptors -ErrorAction Stop) |
|||
|
|||
$direct | Should -HaveCount 2 |
|||
$direct[0].Owner.Sid | Should -Be $expectedFirst |
|||
$direct[1].Owner.Sid | Should -Be $expectedSecond |
|||
} |
|||
|
|||
It 'Set-NTFSOwner -SecurityDescriptor should change both owners in memory without touching the items, piped and passed directly to the parameter' { |
|||
$files = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "SdOwnerSet$_" } |
|||
$before = $files | ForEach-Object -Process { (Get-Acl -LiteralPath $_).GetOwner($sidType).Value } |
|||
$descriptors = @(Get-NTFSSecurityDescriptor -Path $files) |
|||
|
|||
$result = @($descriptors | Set-NTFSOwner -Account 'S-1-1-0' -PassThru -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
($result.FullName -join '|') | Should -Be ($files -join '|') |
|||
$result | ForEach-Object -Process { $_.Owner.Sid | Should -Be 'S-1-1-0' } |
|||
for ($i = 0; $i -lt 2; $i++) { |
|||
(Get-Acl -LiteralPath $files[$i]).GetOwner($sidType).Value | Should -Be $before[$i] |
|||
} |
|||
|
|||
# A direct array binds both descriptors within one ProcessRecord call; the pipeline above rebinds the |
|||
# parameter once per item instead, so only this direct form can catch a handler that always changes the |
|||
# first descriptor of the set. |
|||
$directFiles = 1, 2 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "SdOwnerSetDirect$_" } |
|||
$directDescriptors = @(Get-NTFSSecurityDescriptor -Path $directFiles) |
|||
|
|||
Set-NTFSOwner -SecurityDescriptor $directDescriptors -Account 'S-1-1-0' -ErrorAction Stop |
|||
|
|||
$directDescriptors | ForEach-Object -Process { $_.SecurityDescriptor.GetOwner($sidType).Value | Should -Be 'S-1-1-0' } |
|||
} |
|||
|
|||
It 'Get-NTFSSecurityDescriptor should take piped folder objects, a standard Get-Item object, and a piped custom object by its Path property' { |
|||
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SdPipeParent' -Directory |
|||
$children = foreach ($i in 1, 2) { |
|||
$child = Join-Path -Path $parent -ChildPath "Child$i" |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $child |
|||
New-Item -ItemType Directory -Path $child | Out-Null |
|||
$child |
|||
} |
|||
|
|||
$result = @(Get-ChildItem2 -Path $parent -Directory | Get-NTFSSecurityDescriptor -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 2 |
|||
$result | ForEach-Object -Process { $_ | Should -BeOfType ([Security2.FileSystemSecurity2]) } |
|||
(@($result.FullName) | Sort-Object) | Should -Be (@($children) | Sort-Object) |
|||
foreach ($child in $children) { |
|||
$sd = $result | Where-Object -FilterScript { $_.FullName -eq $child } |
|||
$sd.SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -Be (Get-Acl -LiteralPath $child).GetSecurityDescriptorSddlForm('Access') |
|||
} |
|||
|
|||
$standardItem = Get-Item -LiteralPath $children[0] |
|||
$byStandardItem = @(Get-NTFSSecurityDescriptor -Path $standardItem -ErrorAction Stop) |
|||
$byStandardItem | Should -HaveCount 1 |
|||
$byStandardItem[0].FullName | Should -Be $children[0] |
|||
|
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'SdPipeByProperty' |
|||
$expectedSddl = (Get-Acl -LiteralPath $file).GetSecurityDescriptorSddlForm('Access') |
|||
|
|||
$byProperty = @([pscustomobject]@{ Path = $file } | Get-NTFSSecurityDescriptor -ErrorAction Stop) |
|||
|
|||
$byProperty | Should -HaveCount 1 |
|||
$byProperty[0].FullName | Should -Be $file |
|||
$byProperty[0].SecurityDescriptor.GetSecurityDescriptorSddlForm('Access') | Should -Be $expectedSddl |
|||
} |
|||
} |
|||
@ -0,0 +1,595 @@ |
|||
<# |
|||
Closes TestNeeded gaps that the judged parameter-set matrix found in the item, link, hash, privilege, and |
|||
disk-space cmdlets of the module built in NTFSSecurity\bin\Release: no test piped into Copy-Item2, Get-ChildItem2, |
|||
Get-Item2, Move-Item2, or Remove-Item2; an empty or null -Path/-Target never falls back to the current location; |
|||
missing error categories and output types; and continuation after an earlier item in the same pipeline. It does not |
|||
pin the -Filter dot rules, the Decision 22 behaviors, the PrivilegeEnabler class, or the owner-restore handlers, |
|||
which are covered and decided elsewhere. |
|||
#> |
|||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
|||
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
|||
)] |
|||
param () |
|||
|
|||
BeforeDiscovery { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege' |
|||
} |
|||
|
|||
BeforeAll { |
|||
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
|||
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' |
|||
Import-Module -Name $modulePath -Force -ErrorAction Stop |
|||
$sandbox = New-TestSandbox -Name 'ParameterSetsItems' |
|||
Push-Location -LiteralPath $sandbox |
|||
} |
|||
|
|||
AfterAll { |
|||
Pop-Location |
|||
Remove-TestSandbox -Sandbox $sandbox |
|||
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
|||
} |
|||
|
|||
Describe 'Copy-Item2 and Move-Item2 accept pipeline input' { |
|||
# The cmdlet pages document piping Get-ChildItem2 output into Copy-Item2/Move-Item2, but no existing test pipes |
|||
# anything into either cmdlet. A missing item in the middle of the pipeline must not stop the surrounding items. |
|||
It '<Command> should accept <InputKind> piped by value and still process the surrounding items' -ForEach @( |
|||
@{ Command = 'Copy-Item2'; InputKind = 'plain path strings with a missing item in the middle' } |
|||
@{ Command = 'Copy-Item2'; InputKind = 'Get-ChildItem2 file objects' } |
|||
@{ Command = 'Move-Item2'; InputKind = 'plain path strings with a missing item in the middle' } |
|||
@{ Command = 'Move-Item2'; InputKind = 'Get-ChildItem2 file objects' } |
|||
) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipelineItems' -Directory |
|||
$destination = Join-Path -Path $folder -ChildPath 'Destination' |
|||
$first = Join-Path -Path $folder -ChildPath 'First.txt' |
|||
$second = Join-Path -Path $folder -ChildPath 'Second.txt' |
|||
$missing = Join-Path -Path $folder -ChildPath 'Missing.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination, $first, $second, $missing |
|||
New-Item -ItemType Directory -Path $destination | Out-Null |
|||
Set-Content -LiteralPath $first -Value 'First' |
|||
Set-Content -LiteralPath $second -Value 'Second' |
|||
|
|||
$usesMissingItem = $InputKind -like '*missing item*' |
|||
$items = if ($usesMissingItem) { |
|||
@($first, $missing, $second) |
|||
} |
|||
else { |
|||
@(Get-ChildItem2 -Path $folder -File -ErrorAction Stop) |
|||
} |
|||
|
|||
$result = @($items | & $Command -Destination $destination -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue) |
|||
|
|||
if ($usesMissingItem) { |
|||
$itemErrors | Should -HaveCount 1 |
|||
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*' |
|||
$itemErrors[0].CategoryInfo.Category | Should -Be 'ObjectNotFound' |
|||
$itemErrors[0].TargetObject | Should -Be $missing |
|||
$itemErrors[0].Exception | Should -BeOfType [System.IO.FileNotFoundException] |
|||
} |
|||
else { |
|||
$itemErrors | Should -BeNullOrEmpty |
|||
} |
|||
$result | Should -HaveCount 2 |
|||
$destinationFirst = Join-Path -Path $destination -ChildPath 'First.txt' |
|||
$destinationSecond = Join-Path -Path $destination -ChildPath 'Second.txt' |
|||
@($result.FullName | Sort-Object) | Should -Be (@($destinationFirst, $destinationSecond) | Sort-Object) |
|||
Get-Content -LiteralPath $destinationFirst | Should -Be 'First' |
|||
Get-Content -LiteralPath $destinationSecond | Should -Be 'Second' |
|||
if ($Command -eq 'Copy-Item2') { |
|||
$first | Should -Exist |
|||
$second | Should -Exist |
|||
} |
|||
else { |
|||
$first | Should -Not -Exist |
|||
$second | Should -Not -Exist |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-ChildItem2 accepts pipeline input' { |
|||
# No existing test pipes a path into Get-ChildItem2, although -Path documents taking it by value and by FullName. |
|||
It 'Should accept piped folders by value, write one error for a path that does not exist, and list the items of the other folders' { |
|||
$folderA = New-TestSandboxItem -Sandbox $sandbox -Name 'PipelineFolderA' -Directory |
|||
$folderB = New-TestSandboxItem -Sandbox $sandbox -Name 'PipelineFolderB' -Directory |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('PipelineChildMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
$fileA = Join-Path -Path $folderA -ChildPath 'A.txt' |
|||
$fileB = Join-Path -Path $folderB -ChildPath 'B.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $fileA, $fileB |
|||
Set-Content -LiteralPath $fileA -Value 'A' |
|||
Set-Content -LiteralPath $fileB -Value 'B' |
|||
|
|||
$result = @(@($folderA, $missing, $folderB) | Get-ChildItem2 -ErrorVariable childErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$childErrors | Should -HaveCount 1 |
|||
$childErrors[0].FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*' |
|||
$childErrors[0].CategoryInfo.Category | Should -Be 'ObjectNotFound' |
|||
$childErrors[0].TargetObject | Should -Be $missing |
|||
@($result.FullName | Sort-Object) | Should -Be (@($fileA, $fileB) | Sort-Object) |
|||
} |
|||
|
|||
# Chains a folder object from one call into -Path of a second call, by value (AlphaFS objects, not strings). |
|||
It 'Should accept folder objects piped from Get-ChildItem2 -Directory and list only the files directly below them' { |
|||
$tree = New-TestSandboxItem -Sandbox $sandbox -Name 'ChainedTree' -Directory |
|||
$childFolder = Join-Path -Path $tree -ChildPath 'Child' |
|||
$childFile = Join-Path -Path $childFolder -ChildPath 'Child.log' |
|||
$grandchildFile = Join-Path -Path $childFolder -ChildPath 'Grandchild\Deep.log' |
|||
$rootFile = Join-Path -Path $tree -ChildPath 'Root.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $childFile, $grandchildFile, $rootFile |
|||
New-Item -ItemType Directory -Path (Split-Path -Path $grandchildFile -Parent) -Force | Out-Null |
|||
Set-Content -LiteralPath $childFile -Value 'Child' |
|||
Set-Content -LiteralPath $grandchildFile -Value 'Grandchild' |
|||
Set-Content -LiteralPath $rootFile -Value 'Root' |
|||
|
|||
$result = @(Get-ChildItem2 -Path $tree -Directory -ErrorAction Stop | Get-ChildItem2 -File -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] |
|||
$result[0].FullName | Should -Be $childFile |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-Item2 accepts pipeline input' { |
|||
# No existing test pipes a path into Get-Item2; the output type and Mode were asserted only for non-piped calls. |
|||
It 'Should accept piped items by value, return each in order with its output type, and write one error for a path that does not exist' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'GetItemPipelineFile' |
|||
[IO.File]::SetAttributes($file, [IO.FileAttributes]::Archive) |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'GetItemPipelineFolder' -Directory |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('GetItemPipelineMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
|
|||
$result = @(@($file, $missing, $folder) | Get-Item2 -ErrorVariable itemErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$itemErrors | Should -HaveCount 1 |
|||
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*' |
|||
$itemErrors[0].CategoryInfo.Category | Should -Be 'ObjectNotFound' |
|||
$itemErrors[0].TargetObject | Should -Be $missing |
|||
$result | Should -HaveCount 2 |
|||
$result[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] |
|||
$result[0].FullName | Should -Be $file |
|||
$result[0].Mode | Should -BeExactly '-a---' |
|||
$result[1] | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo] |
|||
$result[1].FullName | Should -Be $folder |
|||
$result[1].Mode | Should -BeExactly 'd----' |
|||
} |
|||
} |
|||
|
|||
Describe 'Remove-Item2 accepts pipeline input' { |
|||
# No existing test pipes a path into Remove-Item2, although its page documents Get-ChildItem2 | Remove-Item2. |
|||
It 'Should accept piped items by value, write one error for a non-empty folder, remove the files, and keep the folder' { |
|||
$fileA = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipelineA' |
|||
$fileB = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipelineB' |
|||
$nonEmptyFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipelineFolder' -Directory |
|||
$kept = Join-Path -Path $nonEmptyFolder -ChildPath 'Keep.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $kept |
|||
Set-Content -LiteralPath $kept -Value 'Keep' |
|||
|
|||
$result = @(@($fileA, $nonEmptyFolder, $fileB) | Remove-Item2 -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$removeErrors | Should -HaveCount 1 |
|||
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'DeleteError,*' |
|||
$removeErrors[0].CategoryInfo.Category | Should -Be 'InvalidData' |
|||
$removeErrors[0].TargetObject | Should -Be $nonEmptyFolder |
|||
$fileA | Should -Not -Exist |
|||
$fileB | Should -Not -Exist |
|||
$nonEmptyFolder | Should -Exist |
|||
Get-Content -LiteralPath $kept | Should -Be 'Keep' |
|||
$result | Should -HaveCount 2 |
|||
@($result.FullName | Sort-Object) | Should -Be (@($fileA, $fileB) | Sort-Object) |
|||
} |
|||
} |
|||
|
|||
Describe 'Empty or null -Path or -Target is rejected instead of falling back to the current location' { |
|||
# GetRelativePath('') returns the current location, so ValidateNotNullOrEmpty is the only guard between an empty |
|||
# value and a recursive delete of the current folder. A probe against the built module confirmed in both editions |
|||
# that the attribute does reject an empty string, $null, a piped empty string, and an empty array element, so this |
|||
# pins verified-safe behavior rather than an assumption. |
|||
It 'Remove-Item2 should reject -Path <CaseName> instead of removing the current location' -ForEach @( |
|||
@{ CaseName = 'as an empty string' } |
|||
@{ CaseName = 'as $null' } |
|||
@{ CaseName = 'as an array with an empty element' } |
|||
@{ CaseName = 'piped as an empty string' } |
|||
) { |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyPathGuard' -Directory |
|||
$keep = Join-Path -Path $folder -ChildPath 'Keep.txt' |
|||
$innerFolder = Join-Path -Path $folder -ChildPath 'Sub' |
|||
$inner = Join-Path -Path $innerFolder -ChildPath 'Inner.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $keep, $innerFolder, $inner |
|||
New-Item -ItemType Directory -Path $innerFolder | Out-Null |
|||
Set-Content -LiteralPath $keep -Value 'Keep' |
|||
Set-Content -LiteralPath $inner -Value 'Inner' |
|||
|
|||
Push-Location -LiteralPath $folder |
|||
try { |
|||
switch ($CaseName) { |
|||
'as an empty string' { |
|||
{ Remove-Item2 -Path '' -Recurse -Force -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.RemoveItem2' |
|||
} |
|||
'as $null' { |
|||
{ Remove-Item2 -Path $null -Recurse -Force -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.RemoveItem2' |
|||
} |
|||
'as an array with an empty element' { |
|||
{ Remove-Item2 -Path @('') -Recurse -Force -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.RemoveItem2' |
|||
} |
|||
'piped as an empty string' { |
|||
{ '' | Remove-Item2 -Recurse -Force -ErrorAction Stop } | |
|||
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.RemoveItem2' |
|||
} |
|||
} |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
|
|||
$keep | Should -Exist |
|||
$inner | Should -Exist |
|||
} |
|||
|
|||
It '<Command> should reject -Path <CaseName> instead of silently using the current location' -ForEach @( |
|||
@{ Command = 'Copy-Item2'; TypeName = 'NTFSSecurity.CopyItem2'; CaseName = 'as an empty string' } |
|||
@{ Command = 'Copy-Item2'; TypeName = 'NTFSSecurity.CopyItem2'; CaseName = 'as $null' } |
|||
@{ Command = 'Copy-Item2'; TypeName = 'NTFSSecurity.CopyItem2'; CaseName = 'as an array with an empty element' } |
|||
@{ Command = 'Move-Item2'; TypeName = 'NTFSSecurity.MoveItem2'; CaseName = 'as an empty string' } |
|||
@{ Command = 'Move-Item2'; TypeName = 'NTFSSecurity.MoveItem2'; CaseName = 'as $null' } |
|||
@{ Command = 'Move-Item2'; TypeName = 'NTFSSecurity.MoveItem2'; CaseName = 'as an array with an empty element' } |
|||
) { |
|||
$destination = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyPathDestination' -Directory |
|||
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyPathSource' |
|||
# An empty path resolves to the current location, so the rows run in a folder of their own: a guard that regressed |
|||
# would copy or move this empty folder and not the whole sandbox into itself. |
|||
$workFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyPathWork' -Directory |
|||
|
|||
# The comma keeps the array: a switch writes to the pipeline, which unrolls @('') into a plain empty string. |
|||
$badPath = switch ($CaseName) { |
|||
'as an empty string' { '' } |
|||
'as $null' { $null } |
|||
'as an array with an empty element' { , @('') } |
|||
} |
|||
|
|||
Push-Location -LiteralPath $workFolder |
|||
try { |
|||
{ & $Command -Path $badPath -Destination $destination -ErrorAction Stop } | |
|||
Should -Throw -ErrorId "ParameterArgumentValidationError,$TypeName" |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
$source | Should -Exist |
|||
$workFolder | Should -Exist |
|||
@(Get-ChildItem -LiteralPath $destination -Force) | Should -BeNullOrEmpty |
|||
} |
|||
|
|||
# "Both link creators" per the proposal; a probe confirmed the same safe, verified behavior for New-NTFSHardLink. |
|||
It '<Command> should reject <CaseName> instead of creating a link to the current folder' -ForEach @( |
|||
@{ Command = 'New-NTFSHardLink'; TypeName = 'NTFSSecurity.NewHardLink'; CaseName = 'an empty -Target' } |
|||
@{ Command = 'New-NTFSHardLink'; TypeName = 'NTFSSecurity.NewHardLink'; CaseName = 'a $null -Target' } |
|||
@{ Command = 'New-NTFSHardLink'; TypeName = 'NTFSSecurity.NewHardLink'; CaseName = 'an empty -Path' } |
|||
@{ Command = 'New-NTFSSymbolicLink'; TypeName = 'NTFSSecurity.NewSymbolicLink'; CaseName = 'an empty -Target' } |
|||
@{ Command = 'New-NTFSSymbolicLink'; TypeName = 'NTFSSecurity.NewSymbolicLink'; CaseName = 'a $null -Target' } |
|||
@{ Command = 'New-NTFSSymbolicLink'; TypeName = 'NTFSSecurity.NewSymbolicLink'; CaseName = 'an empty -Path' } |
|||
) { |
|||
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyGuardTarget' |
|||
$link = Join-Path -Path $sandbox -ChildPath ('EmptyGuardLink-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $link |
|||
|
|||
switch ($CaseName) { |
|||
'an empty -Target' { |
|||
{ & $Command -Path $link -Target '' -ErrorAction Stop } | |
|||
Should -Throw -ErrorId "ParameterArgumentValidationError,$TypeName" |
|||
$link | Should -Not -Exist |
|||
} |
|||
'a $null -Target' { |
|||
{ & $Command -Path $link -Target $null -ErrorAction Stop } | |
|||
Should -Throw -ErrorId "ParameterArgumentValidationError,$TypeName" |
|||
$link | Should -Not -Exist |
|||
} |
|||
'an empty -Path' { |
|||
{ & $Command -Path '' -Target $target -ErrorAction Stop } | |
|||
Should -Throw -ErrorId "ParameterArgumentValidationError,$TypeName" |
|||
Get-Content -LiteralPath $target | Should -Be 'EmptyGuardTarget' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-NTFSHardLink errors, categories, output type, and positional binding' { |
|||
It 'Should accept piped strings by value, write errors with their categories for a folder and a missing path, and return the files in order' { |
|||
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'HardLinkPipelineFile' |
|||
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'HardLinkPipelineFolder' -Directory |
|||
$missing = Join-Path -Path $sandbox -ChildPath ('HardLinkPipelineMissing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) |
|||
$other = New-TestSandboxItem -Sandbox $sandbox -Name 'HardLinkPipelineOther' |
|||
|
|||
$result = @(@($file, $folder, $missing, $other) | Get-NTFSHardLink -ErrorVariable linkErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$linkErrors | Should -HaveCount 2 |
|||
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' |
|||
$linkErrors[0].CategoryInfo.Category | Should -Be 'InvalidArgument' |
|||
$linkErrors[0].TargetObject | Should -Be $folder |
|||
$linkErrors[1].FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*' |
|||
$linkErrors[1].CategoryInfo.Category | Should -Be 'ObjectNotFound' |
|||
$linkErrors[1].TargetObject | Should -Be $missing |
|||
$result | Should -HaveCount 2 |
|||
$result[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] |
|||
$result[0].FullName | Should -Be $file |
|||
$result[1] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] |
|||
$result[1].FullName | Should -Be $other |
|||
(Get-NTFSHardLink $file -ErrorAction Stop).FullName | Should -Be $file |
|||
} |
|||
} |
|||
|
|||
Describe 'Pipeline continuation for the link-creating cmdlets' { |
|||
# Mirrors the PipelineControl.Tests.ps1 Untouched convention locally: a later command that ends the pipeline after |
|||
# the first pass-through object must leave the second link request completely untouched. |
|||
It 'New-NTFSHardLink should stop after the first pass-through object for Select-Object -First 1 and leave the second request untouched' { |
|||
$firstTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinuationHardFirstTarget' |
|||
$secondTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinuationHardSecondTarget' |
|||
$firstLink = Join-Path -Path $sandbox -ChildPath 'ContinuationHardFirstLink.txt' |
|||
$secondLink = Join-Path -Path $sandbox -ChildPath 'ContinuationHardSecondLink.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $firstLink, $secondLink |
|||
$requests = @( |
|||
[pscustomobject]@{ Path = $firstLink; Target = $firstTarget } |
|||
[pscustomobject]@{ Path = $secondLink; Target = $secondTarget } |
|||
) |
|||
|
|||
$result = @($requests | New-NTFSHardLink -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue | Select-Object -First 1) |
|||
|
|||
$linkErrors | Should -BeNullOrEmpty |
|||
$result | Should -HaveCount 1 |
|||
$firstLink | Should -Exist |
|||
$secondLink | Should -Not -Exist |
|||
} |
|||
|
|||
It 'New-NTFSSymbolicLink should stop after the first pass-through object for Select-Object -First 1 and leave the second request untouched' -Skip:(-not $canCreateSymbolicLinks) { |
|||
$firstTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinuationSymbolicFirstTarget' |
|||
$secondTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinuationSymbolicSecondTarget' |
|||
$firstLink = Join-Path -Path $sandbox -ChildPath 'ContinuationSymbolicFirstLink.txt' |
|||
$secondLink = Join-Path -Path $sandbox -ChildPath 'ContinuationSymbolicSecondLink.txt' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $firstLink, $secondLink |
|||
$requests = @( |
|||
[pscustomobject]@{ Path = $firstLink; Target = $firstTarget } |
|||
[pscustomobject]@{ Path = $secondLink; Target = $secondTarget } |
|||
) |
|||
|
|||
$result = @($requests | New-NTFSSymbolicLink -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue | Select-Object -First 1) |
|||
|
|||
$linkErrors | Should -BeNullOrEmpty |
|||
$result | Should -HaveCount 1 |
|||
(Test-Path2 -Path $firstLink) | Should -BeTrue |
|||
(Test-Path2 -Path $secondLink) | Should -BeFalse |
|||
} |
|||
} |
|||
|
|||
Describe 'New-NTFSSymbolicLink creates a link after an earlier failure in the same pipeline' { |
|||
# Every existing piped request fails (the suite runs without the privilege on most hosts), so a successful |
|||
# creation that follows a failed one in the same call was unverified; this is the weakest dimension the matrix |
|||
# found for this cmdlet (Continuation: Partly). |
|||
It 'Should write one ResourceExists error for an existing -Path and still create the links for the requests that follow it' -Skip:(-not $canCreateSymbolicLinks) { |
|||
$existingTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'SuccessAfterFailureExistingTarget' |
|||
$existingPath = New-TestSandboxItem -Sandbox $sandbox -Name 'SuccessAfterFailureExistingPath' |
|||
Set-Content -LiteralPath $existingPath -Value 'ExistingPath' |
|||
$fileTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'SuccessAfterFailureFileTarget' |
|||
Set-Content -LiteralPath $fileTarget -Value 'FileTargetData' |
|||
$fileLink = Join-Path -Path $sandbox -ChildPath 'SuccessAfterFailureFileLink.txt' |
|||
$folderTarget = New-TestSandboxItem -Sandbox $sandbox -Name 'SuccessAfterFailureFolderTarget' -Directory |
|||
$folderLink = Join-Path -Path $sandbox -ChildPath 'SuccessAfterFailureFolderLink' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $fileLink, $folderLink |
|||
$requests = @( |
|||
[pscustomobject]@{ Path = $existingPath; Target = $existingTarget } |
|||
[pscustomobject]@{ Path = $fileLink; Target = $fileTarget } |
|||
[pscustomobject]@{ Path = $folderLink; Target = $folderTarget } |
|||
) |
|||
|
|||
$result = @($requests | New-NTFSSymbolicLink -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$linkErrors | Should -HaveCount 1 |
|||
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*' |
|||
$linkErrors[0].CategoryInfo.Category | Should -Be 'ResourceExists' |
|||
$linkErrors[0].TargetObject | Should -Be $existingPath |
|||
Get-Content -LiteralPath $existingPath | Should -Be 'ExistingPath' |
|||
$result | Should -HaveCount 2 |
|||
$result[0] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] |
|||
$result[0].FullName | Should -Be $fileLink |
|||
$result[1] | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo] |
|||
$result[1].FullName | Should -Be $folderLink |
|||
(Get-Item -LiteralPath $fileLink -Force).LinkType | Should -Be 'SymbolicLink' |
|||
Get-Content -LiteralPath $fileLink | Should -Be 'FileTargetData' |
|||
} |
|||
} |
|||
|
|||
Describe 'Enable-Privileges and Disable-Privileges error paths' { |
|||
# 'Enable Privilege Error' and 'Disable Privilege Error' run silently in many existing cleanups but are never |
|||
# asserted; both IDs contain spaces by design (BaseCmdlets.cs, OtherCmdlets.cs), which this pins as given. |
|||
BeforeAll { |
|||
$privateData = (Get-Module -Name NTFSSecurity).PrivateData |
|||
$savedEnablePrivileges = $privateData['EnablePrivileges'] |
|||
|
|||
# Runs in a child process: removing a privilege from a token is permanent for that process, so a token that |
|||
# holds all four (elevated) ends up holding at most three, and a token that holds none (basic user) stays at |
|||
# none either way. Either way, Enable-Privileges cannot enable all four and must report the error. |
|||
$enablePrivilegesErrorScript = Join-Path -Path $sandbox -ChildPath 'Invoke-EnablePrivilegesWithoutSecurity.ps1' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $enablePrivilegesErrorScript |
|||
Set-Content -LiteralPath $enablePrivilegesErrorScript -Value @' |
|||
param ($ModulePath) |
|||
Import-Module -Name $ModulePath -ErrorAction Stop |
|||
$process = [System.Diagnostics.Process]::GetCurrentProcess() |
|||
try { |
|||
[ProcessPrivileges.ProcessExtensions]::RemovePrivilege($process, [ProcessPrivileges.Privilege]::Security) | Out-Null |
|||
} |
|||
catch { |
|||
} |
|||
Enable-Privileges -ErrorVariable enableErrors -ErrorAction SilentlyContinue |
|||
'COUNT:{0}' -f @($enableErrors).Count |
|||
if ($enableErrors) { |
|||
'ID:{0}' -f $enableErrors[0].FullyQualifiedErrorId |
|||
'CATEGORY:{0}' -f $enableErrors[0].CategoryInfo.Category |
|||
'EXCEPTIONTYPE:{0}' -f $enableErrors[0].Exception.GetType().FullName |
|||
'MESSAGE:{0}' -f $enableErrors[0].Exception.Message |
|||
} |
|||
'@ |
|||
} |
|||
|
|||
AfterAll { |
|||
$privateData['EnablePrivileges'] = $savedEnablePrivileges |
|||
} |
|||
|
|||
AfterEach { |
|||
# A failing test here must not leave a privilege enabled for the tests that follow. |
|||
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
|||
} |
|||
|
|||
It 'Enable-Privileges should write Enable Privilege Error with SecurityError and AdjustPriviledgeException when it cannot enable all four privileges' { |
|||
$output = & (Get-Process -Id $PID).Path -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $enablePrivilegesErrorScript -ModulePath ([IO.Path]::GetFullPath($modulePath)) |
|||
|
|||
($output | Where-Object -FilterScript { $_ -like 'COUNT:*' }) | Should -Be 'COUNT:1' |
|||
($output | Where-Object -FilterScript { $_ -like 'ID:*' }) | Should -Be 'ID:Enable Privilege Error,NTFSSecurity.EnablePrivileges' |
|||
($output | Where-Object -FilterScript { $_ -like 'CATEGORY:*' }) | Should -Be 'CATEGORY:SecurityError' |
|||
($output | Where-Object -FilterScript { $_ -like 'EXCEPTIONTYPE:*' }) | Should -Be 'EXCEPTIONTYPE:Security2.AdjustPriviledgeException' |
|||
($output | Where-Object -FilterScript { $_ -like 'MESSAGE:*' }) | Should -BeLike 'MESSAGE:*Could not enable requested privileges*' |
|||
} |
|||
|
|||
It 'Disable-Privileges should write Disable Privilege Error with SecurityError and AdjustPriviledgeException and no output when nothing is enabled' { |
|||
# EnablePrivileges=$false stops BeginProcessing from auto-enabling the privileges first, which would otherwise |
|||
# always satisfy the "something is enabled" check and hide this error path. |
|||
$privateData['EnablePrivileges'] = $false |
|||
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null |
|||
|
|||
$result = @(Disable-Privileges -PassThru -ErrorVariable disableErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$disableErrors | Should -HaveCount 1 |
|||
$disableErrors[0].FullyQualifiedErrorId | Should -Be 'Disable Privilege Error,NTFSSecurity.DisablePrivileges' |
|||
$disableErrors[0].CategoryInfo.Category | Should -Be 'SecurityError' |
|||
$disableErrors[0].Exception | Should -BeOfType [Security2.AdjustPriviledgeException] |
|||
$disableErrors[0].Exception.Message | Should -Be 'Privileges are not enabled' |
|||
$result | Should -BeNullOrEmpty |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-DiskSpace with several drive letters' { |
|||
# Reuses the free-letter lookup of the existing tests at ItemCmdlets.Tests.ps1:802 and :1063. |
|||
It 'Should return only the system drive and warn for a drive letter without a volume, regardless of order (<Order>)' -ForEach @( |
|||
@{ Order = 'valid letter first' } |
|||
@{ Order = 'invalid letter first' } |
|||
) { |
|||
$used = @((Get-PSDrive -PSProvider FileSystem).Name) + @([System.IO.DriveInfo]::GetDrives() | ForEach-Object -Process { $_.Name.Substring(0, 1) }) |
|||
$letter = [char[]](68..90) | Where-Object -FilterScript { [string] $_ -notin $used } | Select-Object -First 1 |
|||
if (-not $letter) { |
|||
Set-ItResult -Skipped -Because 'every drive letter is in use' |
|||
return |
|||
} |
|||
$missingLetter = "${letter}:" |
|||
$driveLetters = if ($Order -eq 'valid letter first') { @($env:SystemDrive, $missingLetter) } else { @($missingLetter, $env:SystemDrive) } |
|||
|
|||
$result = @(Get-DiskSpace -DriveLetter $driveLetters -WarningVariable spaceWarnings -WarningAction SilentlyContinue -ErrorVariable spaceErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$spaceErrors | Should -BeNullOrEmpty |
|||
$spaceWarnings.Message | Should -Be "Could not get drive details for '$missingLetter'" |
|||
$result | Should -HaveCount 1 |
|||
$result[0].DriveName | Should -Be ('{0}\' -f $env:SystemDrive) |
|||
} |
|||
|
|||
It 'Should accept a lower-case drive letter positionally and report the same total size as .NET' { |
|||
$systemDrive = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive |
|||
|
|||
$result = @(Get-DiskSpace $env:SystemDrive.ToLowerInvariant() -ErrorAction Stop) |
|||
|
|||
$result | Should -HaveCount 1 |
|||
$result[0].TotalNumberOfBytes | Should -Be $systemDrive.TotalSize |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-Privileges compared with an independent source' { |
|||
# whoami and the library name some privileges differently; these five are named consistently via 'Se<Name>Privilege'. |
|||
It 'Should match whoami /priv for the privileges that whoami and the library name the same way' { |
|||
$whoamiPrivileges = & whoami.exe /priv /fo csv | Select-Object -Skip 1 | ConvertFrom-Csv -Header 'Name', 'Description', 'State' |
|||
$libraryPrivileges = @(Get-Privileges -ErrorAction Stop) |
|||
|
|||
foreach ($name in 'ChangeNotify', 'TakeOwnership', 'Restore', 'Backup', 'Security') { |
|||
$whoamiName = 'Se{0}Privilege' -f $name |
|||
$whoamiEntry = @($whoamiPrivileges | Where-Object -Property Name -EQ -Value $whoamiName) |
|||
$libraryEntry = @($libraryPrivileges | Where-Object -Property Privilege -EQ -Value $name) |
|||
|
|||
if ($whoamiEntry.Count -gt 0) { |
|||
$libraryEntry | Should -HaveCount 1 -Because "Get-Privileges should list $name when whoami lists $whoamiName" |
|||
$libraryEntry[0] | Should -BeOfType [ProcessPrivileges.PrivilegeAndAttributes] |
|||
# The State column is localized (a German Windows prints Aktiviert), so the states are compared only where |
|||
# whoami prints the English words; the privilege names above are not localized. |
|||
if ($whoamiEntry[0].State -in 'Enabled', 'Disabled') { |
|||
$libraryEntry[0].PrivilegeState.ToString() | Should -Be $whoamiEntry[0].State |
|||
$isEnabledByAttributes = ($libraryEntry[0].PrivilegeAttributes -band [ProcessPrivileges.PrivilegeAttributes]::Enabled) -eq [ProcessPrivileges.PrivilegeAttributes]::Enabled |
|||
$isEnabledByAttributes | Should -Be ($whoamiEntry[0].State -eq 'Enabled') |
|||
} |
|||
} |
|||
else { |
|||
$libraryEntry | Should -BeNullOrEmpty -Because "Get-Privileges should not list $name when whoami does not list $whoamiName" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Describe 'Get-FileHash2 with pipeline input and ordered failures' { |
|||
It 'Should accept Get-ChildItem2 objects piped by value, skip a folder, and match Get-FileHash for each file' { |
|||
$dir = New-TestSandboxItem -Sandbox $sandbox -Name 'HashPipelineDir' -Directory |
|||
$fileA = Join-Path -Path $dir -ChildPath 'A.txt' |
|||
$fileB = Join-Path -Path $dir -ChildPath 'B.txt' |
|||
$subFolder = Join-Path -Path $dir -ChildPath 'Sub' |
|||
Assert-TestSandboxPath -Sandbox $sandbox -Path $fileA, $fileB, $subFolder |
|||
Set-Content -LiteralPath $fileA -Value 'HashA' |
|||
Set-Content -LiteralPath $fileB -Value 'HashB' |
|||
New-Item -ItemType Directory -Path $subFolder | Out-Null |
|||
|
|||
$result = @(Get-ChildItem2 -Path $dir -ErrorVariable childErrors -ErrorAction SilentlyContinue | Get-FileHash2 -ErrorVariable hashErrors -ErrorAction SilentlyContinue) |
|||
|
|||
$childErrors | Should -BeNullOrEmpty |
|||
$hashErrors | Should -BeNullOrEmpty |
|||
$result | Should -HaveCount 2 |
|||
@($result.Name | Sort-Object) | Should -Be @('A.txt', 'B.txt') |
|||
foreach ($item in $result) { |
|||
$sourcePath = Join-Path -Path $dir -ChildPath $item.Name |
|||
$item.Hash | Should -BeExactly (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash |
|||
$item.Algorithm | Should -Be 'SHA256' |
|||
$item.PSObject.TypeNames[0] | Should -Be 'Alphaleonis.Win32.Filesystem.FileInfo+Hash' |
|||
} |
|||
|
|||
# The algorithm comes from a variable, like the data of FileHash.Tests.ps1: the test compares with Get-FileHash and uses no hash for security. |
|||
$legacyAlgorithm = 'MD5' |
|||
$md5Result = @(Get-ChildItem -LiteralPath $dir -File | Get-FileHash2 -Algorithm $legacyAlgorithm -ErrorAction Stop) |
|||
|
|||
$md5Result | Should -HaveCount 2 |
|||
foreach ($item in $md5Result) { |
|||
$sourcePath = Join-Path -Path $dir -ChildPath $item.Name |
|||
$item.Hash | Should -BeExactly (Get-FileHash -LiteralPath $sourcePath -Algorithm $legacyAlgorithm).Hash |
|||
} |
|||
} |
|||
|
|||
It 'Should write GetHashError with its category for a locked file first, hash the file that follows it, and resolve a relative path' { |
|||
$locked = New-TestSandboxItem -Sandbox $sandbox -Name 'HashLocked' |
|||
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'HashFirstFollowing' |
|||
Set-Content -LiteralPath $first -Value 'FollowingContent' |
|||
$stream = [IO.File]::Open($locked, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) |
|||
try { |
|||
$result = @(Get-FileHash2 -Path $locked, $first -ErrorVariable hashErrors -ErrorAction SilentlyContinue) |
|||
} |
|||
finally { |
|||
$stream.Dispose() |
|||
} |
|||
|
|||
$hashErrors | Should -HaveCount 1 |
|||
$hashErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHashError,*' |
|||
$hashErrors[0].CategoryInfo.Category | Should -Be 'WriteError' |
|||
$hashErrors[0].TargetObject | Should -Be $locked |
|||
$result | Should -HaveCount 1 |
|||
$result[0].Name | Should -BeExactly (Split-Path -Path $first -Leaf) |
|||
$result[0].Hash | Should -BeExactly (Get-FileHash -LiteralPath $first -Algorithm SHA256).Hash |
|||
|
|||
Push-Location -LiteralPath (Split-Path -Path $first -Parent) |
|||
try { |
|||
$relativeResult = Get-FileHash2 -Path (Split-Path -Path $first -Leaf) -ErrorAction Stop |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
|
|||
$relativeResult.Hash | Should -BeExactly $result[0].Hash |
|||
} |
|||
} |
|||
Loading…
Reference in new issue