From 1dec3893c113b472598c97b3bc1a84e504a3c972 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sat, 10 Oct 2026 03:33:18 +0000 Subject: [PATCH] test(lab): give each new fixture a new account for the effective-access case When an account is deleted and created again with the same name, a Kerberos S4U logon for it keeps returning the SID and the groups of the deleted account for a while, on the domain controller, the client, and the file server. The matrix deletes the fixture after each cell and creates it for the next, so the effective-access tests of the Admin role found no access for the new account in cells that followed within minutes (Windows Server 2022 cell, candidate and baseline alike, shown by a probe that creates the accounts in a loop). A new fixture now gets NtfsLiveSubject and four digits; a fixture that exists keeps its account. The end-state check of the matrix also reports leftover scheduled tasks, stage folders, standard users, and probe accounts, which the review asked for. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- Tests/Lab/Acceptance/Run-MatrixSequence.ps1 | 3 ++- Tests/Lab/Acceptance/Test-MatrixCleanup.ps1 | 17 ++++++++++---- Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 | 26 +++++++++++++++++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Tests/Lab/Acceptance/Run-MatrixSequence.ps1 b/Tests/Lab/Acceptance/Run-MatrixSequence.ps1 index 76d91d6..17124a4 100644 --- a/Tests/Lab/Acceptance/Run-MatrixSequence.ps1 +++ b/Tests/Lab/Acceptance/Run-MatrixSequence.ps1 @@ -89,7 +89,8 @@ foreach ($fileServerName in $cells) { & (Join-Path -Path $kit -ChildPath 'Test-MatrixCleanup.ps1') -Mode Verify -SidFile $sidFile -OutFile (Join-Path -Path $cell -ChildPath 'cleanup-3-verify.log') @common $verify = Get-Content -LiteralPath (Join-Path -Path $cell -ChildPath 'cleanup-3-verify.log') -Raw $clean = ($verify -match 'matrix-cleanup-Verify-DONE') -and ($verify -notmatch 'OU NTFSSecurityLive: True') -and ($verify -notmatch 'accounts: NtfsLive') -and - ($verify -notmatch 'share=True|C:\\NTFSSecurityLive=True|C:\\NTFSSecurityLab=True|NtfsLiveLocal=True') -and ($verify -notmatch 'profiles=[1-9]') -and ($verify -notmatch ': [1-9]\d* fixture member') + ($verify -notmatch 'share=True|C:\\NTFSSecurityLive=True|C:\\NTFSSecurityLab=True|NtfsLiveLocal=True') -and ($verify -notmatch 'profiles=[1-9]') -and ($verify -notmatch ': [1-9]\d* fixture member') -and + ($verify -notmatch 'probe accounts: [1-9]') -and ($verify -notmatch 'residue: [^\r\n]*=[1-9]') Write-Sequence ("cell $fileServerName cleanup verdict from the verify log: {0}" -f $(if ($clean) { 'CLEAN' } else { 'DIRTY (read cleanup-3-verify.log)' })) } } diff --git a/Tests/Lab/Acceptance/Test-MatrixCleanup.ps1 b/Tests/Lab/Acceptance/Test-MatrixCleanup.ps1 index da49cb2..292300a 100644 --- a/Tests/Lab/Acceptance/Test-MatrixCleanup.ps1 +++ b/Tests/Lab/Acceptance/Test-MatrixCleanup.ps1 @@ -11,7 +11,8 @@ param ( # Independent end-state check of the fixture of Invoke-NTFSSecurityLabTest.ps1 in a lab (Windows PowerShell 5.1, on the host). Snapshot # records the SIDs of the NtfsLive* accounts while the fixture exists. Verify reads the domains and every machine again, and reports # the organizational unit, the accounts, the share, the folders, the local group, the memberships of Administrators, Access Control -# Assistance Operators, and Remote Management Users, and the profiles of those SIDs. The result is judged from this log, never from +# Assistance Operators, and Remote Management Users, and the profiles of those SIDs, and what the suite runs and the probes of the kit leave +# behind (scheduled tasks, stage folders, standard users, probe accounts of the domain). The result is judged from this log, never from # the wrapper of the controller or a global error count. Repair is for a run whose removal failed: with the SIDs of the snapshot, it # removes what that run left on the machines (the memberships, also of orphaned SIDs, which net localgroup deletes by SID; the share; the # local group; the folders) and then reports like Verify. @@ -28,16 +29,17 @@ param ( $domain = Get-ADDomain $unit = Get-ADOrganizationalUnit -LDAPFilter '(ou=NTFSSecurityLive)' -SearchBase $domain.DistinguishedName -SearchScope OneLevel -Server $domain.PDCEmulator [pscustomobject]@{ - Domain = $domain.DNSRoot - Unit = [bool] $unit - Sids = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsLive*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator -Properties objectSid, sAMAccountName | + Domain = $domain.DNSRoot + Unit = [bool] $unit + Sids = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsLive*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator -Properties objectSid, sAMAccountName | ForEach-Object -Process { '{0}={1}' -f $_.sAMAccountName, $_.objectSid.Value }) + ProbeAccounts = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsProbe*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator).Count } } $directory = @(foreach ($name in $DomainController) { Invoke-LabCommand -ComputerName $name -ActivityName "Read the fixture of $name" -ScriptBlock $directoryScript @labCommand }) foreach ($state in $directory) { - '{0,-14} OU NTFSSecurityLive: {1,-5} NtfsLive* accounts: {2}' -f $state.Domain, $state.Unit, ($(if ($state.Sids) { $state.Sids -join ', ' } else { 'none' })) + '{0,-14} OU NTFSSecurityLive: {1,-5} NtfsLive* accounts: {2}; probe accounts: {3}' -f $state.Domain, $state.Unit, ($(if ($state.Sids) { $state.Sids -join ', ' } else { 'none' })), $state.ProbeAccounts } if ($Mode -eq 'Snapshot') { @@ -65,6 +67,10 @@ param ( LocalGroup = [bool] (Get-LocalGroup -Name 'NtfsLiveLocal' -ErrorAction SilentlyContinue) Groups = $groups -join '; ' Profiles = @(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.SID -in $Sid }).Count + # What the suite runs and the probes of the kit leave behind: scheduled tasks, stage folders, and standard users + Tasks = @(Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.TaskName -like 'NtfsMatrix*' }).Count + Stages = @('C:\NtfsMatrixLocal', 'C:\NtfsMatrixProbe' | Where-Object -FilterScript { Test-Path -LiteralPath $_ }).Count + Users = @(Get-LocalUser -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Name -like 'NtfsProbe*' }).Count } } @@ -105,6 +111,7 @@ param ( $state = Invoke-LabCommand -ComputerName $name -ActivityName "Check $name" -ScriptBlock $machineScript -ArgumentList (, $sids) @labCommand '{0,-9} share={1} C:\NTFSSecurityLive={2} C:\NTFSSecurityLab={3} NtfsLiveLocal={4} profiles={5}' -f $name, $state.Share, $state.ShareRoot, $state.Payload, $state.LocalGroup, $state.Profiles ' {0}' -f $state.Groups + ' residue: scheduled tasks={0} stage folders={1} probe users={2}' -f $state.Tasks, $state.Stages, $state.Users } } diff --git a/Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 b/Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 index 683aee9..9917cac 100644 --- a/Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 +++ b/Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 @@ -128,15 +128,16 @@ $roleAccounts = [ordered]@{ ServerAdmin = 'NtfsLiveServerAdmin' Admin = 'NtfsLiveAdmin' } -$subjectAccount = 'NtfsLiveSubject' +$subjectBaseName = 'NtfsLiveSubject' $orphanAccount = 'NtfsLiveOrphan' $foreignAccount = 'NtfsLiveForeign' # The rights that the entries of the foreign accounts grant on the folder of case 9, by position $foreignRights = 'ReadAndExecute', 'Modify', 'Write' $localGroupName = 'NtfsLiveLocal' +# The members of NtfsLiveInner follow when the name of the account of case 3 is known $groupMembers = @{ NtfsLiveDelegates = @('NtfsLiveDelegate') - NtfsLiveInner = @('NtfsLiveSubject') + NtfsLiveInner = @() NtfsLiveOuter = @('NtfsLiveInner') } # A name that no DNS server resolves (RFC 2606) @@ -300,6 +301,19 @@ function ConvertFrom-LabTestResult { } #region Remote script blocks +# Runs on the domain controller: returns the names of the accounts of case 3 that the organizational unit already has. +$findSubjectScript = { + param ($OrganizationalUnitName, $BaseName) + + $ErrorActionPreference = 'Stop' + Import-Module -Name ActiveDirectory + $domain = Get-ADDomain + $path = 'OU={0},{1}' -f $OrganizationalUnitName, $domain.DistinguishedName + if (Get-ADOrganizationalUnit -LDAPFilter "(ou=$OrganizationalUnitName)" -SearchBase $domain.DistinguishedName -SearchScope OneLevel -Server $domain.PDCEmulator) { + Get-ADUser -LDAPFilter "(sAMAccountName=$BaseName*)" -SearchBase $path -Server $domain.PDCEmulator | ForEach-Object -Process { $_.SamAccountName } + } +} + # Runs on the domain controller: creates or updates the accounts and groups in their organizational unit, pushes them # to the other domain controllers of the domain, and returns their SIDs. $accountScript = { @@ -1060,6 +1074,14 @@ $modules = @( ) Write-LabProgress 'Preparing the accounts, the file server, and the client' +# When an account is deleted and created again with the same name, a Kerberos S4U logon for it keeps returning the SID and the groups of +# the deleted account for a while: on the domain controller, the client, and the file server of the operating-system matrix, for every +# version of the module. The Authz functions behind Get-NTFSEffectiveAccess log an account on this way, so the cmdlet returned no access +# for the new account. A new fixture therefore gets a name for the account of case 3 that no earlier fixture used; a fixture that +# exists keeps its account. +$existingSubjects = @(Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Look for the account of case 3' -ScriptBlock $findSubjectScript -ArgumentList $organizationalUnitName, $subjectBaseName @labCommand) +$subjectAccount = if ($existingSubjects) { [string]$existingSubjects[0] } else { '{0}{1:D4}' -f $subjectBaseName, (Get-Random -Minimum 0 -Maximum 10000) } +$groupMembers['NtfsLiveInner'] = @($subjectAccount) $passwords = @{} foreach ($name in @($roleAccounts.Values) + $subjectAccount) { $passwords[$name] = New-LabPassword