You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

224 lines
13 KiB

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Pester passes the data to the blocks of the container.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'The tests read the variables that BeforeAll sets.')]
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[string] $PesterPath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1'
)
# Diagnostic of the acceptance in Acceptance-2026-10-09-quality-gate-paths.md, not part of it: why do the Select-Object rows of case 10
# fail on the base of the branch without a message? It runs the bodies of those tests (Assert-LabPipelineStop and
# Assert-LabDownstreamFailure of NTFSSecurity.Live.Tests.ps1) against files in a new folder below TEMP, with the settings of the
# runner (Pester 5.7.1, ErrorActionPreference Stop, detailed plain text), and lists for each test its result and error records, and
# the state of the items afterwards. One module build in one edition per process, never imported into another session; the script
# removes its own folder at the end after it has checked the path. Windows only. For example:
# powershell.exe -NoProfile -File Probe-LaterCommand.ps1 -ModulePath <folder with NTFSSecurity.psd1> -OutFile <result.txt>
$ErrorActionPreference = 'Stop'
Import-Module -Name (Join-Path -Path $PesterPath -ChildPath 'Pester.psd1') -Force
$root = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('mute-probe-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $root
$account = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$lines = New-Object -TypeName 'System.Collections.Generic.List[string]'
try {
$container = New-PesterContainer -ScriptBlock {
param ($ModulePath, $Root, $Account)
BeforeAll {
Import-Module -Name (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.psd1') -Force -ErrorAction Stop
$everyone = 'S-1-1-0'
$administrators = 'S-1-5-32-544'
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
$privateData['EnablePrivileges'] = $false
$account = $Account
function Get-ProbeOwner {
param ([string] $Path)
(Get-Acl -LiteralPath $Path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value
}
function New-ProbeFolder {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$path = Join-Path -Path $Root -ChildPath $Name
$null = New-Item -ItemType Directory -Path $path -Force
$path
}
function New-ProbePair {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$directory = New-ProbeFolder -Name $Name
foreach ($item in 'First', 'Second') {
Set-Content -LiteralPath (Join-Path -Path $directory -ChildPath "$item.txt") -Value $item -NoNewline
}
@{ Directory = $directory; First = (Join-Path -Path $directory -ChildPath 'First.txt'); Second = (Join-Path -Path $directory -ChildPath 'Second.txt') }
}
$cases = @{
'Remove-Item2' = @{
Prepare = { param ($Slug) New-ProbePair -Name "RemoveItem2-$Slug" }
Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Copy-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "CopyItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "CopyItem2-$Slug-To"; $c }
Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath 'Second.txt')) }
}
'Move-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "MoveItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "MoveItem2-$Slug-To"; $c }
Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Set-NTFSOwner' = @{
Prepare = { param ($Slug) New-ProbePair -Name "SetOwner-$Slug" }
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) (Get-ProbeOwner -Path $Context.Second) -eq $administrators }
}
'Set-NTFSSecurityDescriptor' = @{
Prepare = {
param ($Slug)
$c = New-ProbePair -Name "SetDescriptor-$Slug"
$c.Descriptors = @(Get-NTFSSecurityDescriptor -Path $c.First, $c.Second -ErrorAction Stop)
Add-NTFSAccess -SecurityDescriptor $c.Descriptors -Account $everyone -AccessRights ReadData -ErrorAction Stop
$c
}
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (@((Get-Acl -LiteralPath $Context.Second).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $everyone }).Count) }
}
}
$streamCases = @{
'Set-NTFSSecurityDescriptor/verbose' = @{
Prepare = $cases['Set-NTFSSecurityDescriptor'].Prepare
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -Verbose -ErrorAction SilentlyContinue 4>&1 }
Untouched = $cases['Set-NTFSSecurityDescriptor'].Untouched
RecordType = [System.Management.Automation.VerboseRecord]
}
'Set-NTFSOwner/debug' = @{
Prepare = $cases['Set-NTFSOwner'].Prepare
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -ErrorAction SilentlyContinue 5>&1 }
Untouched = $cases['Set-NTFSOwner'].Untouched
RecordType = [System.Management.Automation.DebugRecord]
}
}
function Assert-ProbePipelineStop {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$Error.Clear()
$result = @(& $Case.Run $context | Select-Object -First 1)
$result | Should -HaveCount 1
if ($Case.RecordType) {
$result[0] | Should -BeOfType $Case.RecordType
}
$Error.Count | Should -Be 0
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
function Assert-ProbeDownstreamFailure {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$emitted = 0
$caught = $null
$Error.Clear()
try {
& $Case.Run $context | ForEach-Object -Process {
$emitted++
throw 'Downstream failure'
}
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
$emitted | Should -Be 1
@($Error | Where-Object -FilterScript { $_.Exception.Message -notlike '*Downstream failure*' }) | Should -BeNullOrEmpty
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
}
Describe 'Mirror of the later-command tests' {
It '<Name> should stop after the first object for Select-Object -First 1' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbePipelineStop -Case $cases[$Name] -Slug 'Select'
}
It '<Name> should stop after the first object for throw' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbeDownstreamFailure -Case $cases[$Name] -Slug 'Throw'
}
It '<Key> should stop at the message for Select-Object -First 1' -ForEach @(
@{ Key = 'Set-NTFSSecurityDescriptor/verbose'; Stream = 'verbose' }, @{ Key = 'Set-NTFSOwner/debug'; Stream = 'debug' }
) {
Assert-ProbePipelineStop -Case $streamCases[$Key] -Slug ('{0}Select' -f $Stream) -Stream $Stream
}
}
} -Data @{ ModulePath = $ModulePath; Root = $root; Account = $account }
$configuration = New-PesterConfiguration
$configuration.Run.Container = $container
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Detailed'
$configuration.Output.RenderMode = 'Plaintext'
$lines.Add(('Edition {0} {1}; module {2}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion, $ModulePath))
$lines.Add('--- Pester output')
$output = & { Invoke-Pester -Configuration $configuration } *>&1
$result = @($output | Where-Object -FilterScript { $_ -is [Pester.Run] }) | Select-Object -First 1
foreach ($entry in @($output | Where-Object -FilterScript { $_ -isnot [Pester.Run] })) { $lines.Add('{0}' -f $entry) }
$lines.Add('--- Results')
foreach ($test in $result.Tests) {
$messages = @(@($test.ErrorRecord) | Where-Object -FilterScript { $_ } | ForEach-Object -Process { ($_.ToString() -split '\r?\n')[0] })
$lines.Add(('{0} | {1} | error records: {2} | {3}' -f $test.Result, $test.ExpandedName, @($test.ErrorRecord).Count, ($messages -join ' // ')))
}
$lines.Add(('Totals: passed {0}, failed {1}, not run {2}; result {3}' -f $result.PassedCount, $result.FailedCount, $result.NotRunCount, $result.Result))
$lines.Add('--- State of the items after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Sort-Object -Property Name) {
$files = @(Get-ChildItem -LiteralPath $folder.FullName -File | ForEach-Object -Process { $_.Name })
$lines.Add(('{0}: {1}' -f $folder.Name, ($files -join ', ')))
}
$lines.Add('--- Owner (SetOwner folders) and explicit entry for Everyone (SetDescriptor folders) after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Where-Object -FilterScript { $_.Name -like 'SetOwner-*' -or $_.Name -like 'SetDescriptor-*' } | Sort-Object -Property Name) {
foreach ($name in 'First.txt', 'Second.txt') {
$path = Join-Path -Path $folder.FullName -ChildPath $name
$acl = Get-Acl -LiteralPath $path
if ($folder.Name -like 'SetOwner-*') {
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
$lines.Add(('{0}\{1}: owner {2}' -f $folder.Name, $name, $(if ($owner -eq 'S-1-5-32-544') { 'Administrators (as created)' } elseif ($owner -eq $account) { 'the account of the run (changed)' } else { $owner })))
}
else {
$entries = @($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' })
$lines.Add(('{0}\{1}: explicit entry for Everyone: {2}' -f $folder.Name, $name, $(if ($entries.Count) { 'yes (changed)' } else { 'no (as created)' })))
}
}
}
}
finally {
$full = [System.IO.Path]::GetFullPath($root)
if ($full.StartsWith([System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()), [System.StringComparison]::OrdinalIgnoreCase) -and (Split-Path -Path $full -Leaf) -like 'mute-probe-*') {
Remove-Item -LiteralPath $full -Recurse -Force -ErrorAction SilentlyContinue
}
Set-Content -LiteralPath $OutFile -Value $lines -Encoding utf8
}