Browse Source

test(lab): remove the stage of the suite runner and probe re-created accounts

The suite runner removes its stage on a machine after it has copied the results
back; after an early stop, the stage stays for the diagnosis. The end-state check
counts the items in the stage folders and the scheduled tasks and standard users
of the kit, and -Mode Repair removes what is left of the stage and the tasks.

Probe-AccountRecreation.ps1 deletes an account and creates it again with the same
name in a loop, logs the user on with Kerberos S4U on the domain controller, the
client, and the file server, and asks Get-NTFSEffectiveAccess of each module under
test. It shows that Windows returns the old SID and groups, whichever version of
the module asks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/119/head
Raimund Andree 2 days ago
parent
commit
3da87ec3f3
  1. 151
      Tests/Lab/Acceptance/Probe-AccountRecreation.ps1
  2. 13
      Tests/Lab/Acceptance/Run-MatrixLocalSuite.ps1
  3. 18
      Tests/Lab/Acceptance/Test-MatrixCleanup.ps1

151
Tests/Lab/Acceptance/Probe-AccountRecreation.ps1

@ -0,0 +1,151 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string[]] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[string] $Client = 'OSWin11E',
[string] $DomainController = 'OSDC1',
[string] $FileServer = 'OSFile22',
[ValidateRange(2, 20)] [int] $Rounds = 4,
[string] $LabName = 'NtfsSecurityOsMatrixLab'
)
# Probe of the groups that a computer reports for an account that was deleted and created again with the same name (Decision 24). In each
# round it creates a user in a group that is in another group, with the same names and new SIDs, and a folder on the file server whose DACL
# grants the outer group ReadAndExecute. Then it logs the user on with Kerberos S4U, like the oracle of the live tests does, on the domain
# controller, the client, and the file server, and asks from the client, in a new process for each module, for the effective access of the
# account on the folder by name and by SID, with the default server name and with the name of the file server. -ModulePath takes module
# folders as label=path, such as baseline=C:\Build\NTFSSecurity. From the second round on, a computer that still holds the deleted account
# reports its SID, and every module reports no access (Synchronize only), whichever its version. The probe removes everything it created; the
# accounts, the folder, and the files on the client are named NtfsProbe*, so Test-MatrixCleanup.ps1 reports a leftover. The password of the
# user is random and exists only in memory. Windows PowerShell 5.1 on the Hyper-V host, with AutomatedLab.
& {
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$modules = @(
foreach ($entry in @($ModulePath | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })) {
$label, $path = $entry -split '=', 2
if (-not $path -or -not (Test-Path -LiteralPath (Join-Path -Path $path -ChildPath 'NTFSSecurity.psd1'))) { throw "-ModulePath takes label=folder with a module; '$entry' has none." }
[pscustomobject]@{ Label = $label; Path = $path }
}
)
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
$domainName = (Get-Lab).Domains[0].Name
$netBiosName = $domainName.Split('.')[0].ToUpperInvariant()
$dcSession = New-LabPSSession -ComputerName $DomainController
$clientSession = New-LabPSSession -ComputerName $Client
$serverSession = New-LabPSSession -ComputerName $FileServer
$machines = [ordered]@{ $DomainController = $dcSession; $Client = $clientSession; $FileServer = $serverSession }
$userName = 'NtfsProbeSubject'
$innerName = 'NtfsProbeInner'
$outerName = 'NtfsProbeOuter'
$folderName = 'NtfsProbeRecreation'
$stageName = 'C:\NtfsProbeModules'
$report = New-Object -TypeName 'System.Collections.Generic.List[string]'
$bytes = New-Object -TypeName 'byte[]' -ArgumentList 24
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
try { $generator.GetBytes($bytes) } finally { $generator.Dispose() }
# Base64 has upper case and lower case letters and digits; the suffix adds the other classes of a domain's complexity rules.
$secret = New-Object -TypeName 'System.Security.SecureString'
foreach ($character in ([Convert]::ToBase64String($bytes) + '!a1Z').ToCharArray()) { $secret.AppendChar($character) }
$secret.MakeReadOnly()
$removeOnDcScript = {
param ($User, $Inner, $Outer)
Import-Module -Name ActiveDirectory
foreach ($name in $User) { Get-ADUser -Filter "SamAccountName -eq '$name'" | Remove-ADUser -Confirm:$false }
foreach ($name in $Inner, $Outer) { Get-ADGroup -Filter "SamAccountName -eq '$name'" | Remove-ADGroup -Confirm:$false }
}
$createOnDcScript = {
param ($User, $Inner, $Outer, [securestring] $Secret)
Import-Module -Name ActiveDirectory
$null = New-ADGroup -Name $Outer -SamAccountName $Outer -GroupScope Global
$null = New-ADGroup -Name $Inner -SamAccountName $Inner -GroupScope Global
Add-ADGroupMember -Identity $Outer -Members $Inner
$null = New-ADUser -Name $User -SamAccountName $User -UserPrincipalName ('{0}@{1}' -f $User, (Get-ADDomain).DNSRoot) -AccountPassword $Secret -Enabled $true
Add-ADGroupMember -Identity $Inner -Members $User
[pscustomobject]@{ User = (Get-ADUser -Identity $User).SID.Value; Outer = (Get-ADGroup -Identity $Outer).SID.Value }
}
$setFolderScript = {
param ($Folder, $OuterSid)
$path = Join-Path -Path $env:SystemDrive -ChildPath $Folder
if (-not (Test-Path -LiteralPath $path)) { $null = New-Item -ItemType Directory -Path $path }
$acl = New-Object -TypeName 'System.Security.AccessControl.DirectorySecurity'
$acl.SetAccessRuleProtection($true, $false)
foreach ($entry in @(@('S-1-5-32-544', 'FullControl'), @('S-1-5-18', 'FullControl'), @($OuterSid, 'ReadAndExecute'))) {
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ([Security.Principal.SecurityIdentifier] $entry[0]), $entry[1], 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
}
Set-Acl -LiteralPath $path -AclObject $acl
}
$tokenScript = {
param ($User, $Domain, $UserSid, $OuterSid)
try {
$identity = New-Object -TypeName 'System.Security.Principal.WindowsIdentity' -ArgumentList ('{0}@{1}' -f $User, $Domain)
$groups = @($identity.Groups | ForEach-Object -Process { $_.Value })
'S4U token of the {0} account, outer group {1}' -f $(if ($identity.User.Value -eq $UserSid) { 'CURRENT' } else { 'OLD' }), ($groups -contains $OuterSid)
}
catch { 'S4U logon failed: ' + $_.Exception.Message }
}
$effectiveScript = {
param ($ModuleFolder, $Folder, $Server, $Domain, $NetBios, $User, $UserSid)
Import-Module -Name (Join-Path -Path $ModuleFolder -ChildPath 'NTFSSecurity.psd1') -Force
$unc = '\\{0}.{1}\C$\{2}' -f $Server, $Domain, $Folder
$name = '{0}\{1}' -f $NetBios, $User
function Measure-Answer {
param ([hashtable] $Arguments)
$result = @(Get-NTFSEffectiveAccess @Arguments -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable failures)
$value = if ($result.Count) { '0x{0:X}' -f ([long] $result[0].AccessRights) } else { 'none' }
if (@($failures).Count) { $value += ' ERR ' + $failures[0].Exception.Message }
$value
}
$serverName = '{0}.{1}' -f $Server, $Domain
'effective access by name {0}, by name and server {1}, by SID {2}, by SID and server {3}' -f
(Measure-Answer -Arguments @{ Path = $unc; Account = $name }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $name; ServerName = $serverName }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $UserSid }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $UserSid; ServerName = $serverName })
}
$runEffectiveScript = {
param ($Stage, $ModuleLabel, $ScriptText, $Folder, $Server, $Domain, $NetBios, $User, $UserSid)
$block = [scriptblock]::Create($ScriptText)
$powershell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
& $powershell -NoProfile -ExecutionPolicy Bypass -Command $block -args (Join-Path -Path $Stage -ChildPath $ModuleLabel), $Folder, $Server, $Domain, $NetBios, $User, $UserSid
}
try {
Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName
Invoke-Command -Session $clientSession -ArgumentList $stageName -ScriptBlock { param ($Stage) if (Test-Path -LiteralPath $Stage) { Remove-Item -LiteralPath $Stage -Recurse -Force }; $null = New-Item -ItemType Directory -Path $Stage -Force }
foreach ($module in $modules) {
Invoke-Command -Session $clientSession -ArgumentList (Join-Path -Path $stageName -ChildPath $module.Label) -ScriptBlock { param ($Path) $null = New-Item -ItemType Directory -Path $Path -Force }
Copy-Item -Path (Join-Path -Path $module.Path -ChildPath '*') -Destination (Join-Path -Path $stageName -ChildPath $module.Label) -ToSession $clientSession -Recurse -Force
}
for ($round = 1; $round -le $Rounds; $round++) {
$created = Invoke-Command -Session $dcSession -ScriptBlock $createOnDcScript -ArgumentList $userName, $innerName, $outerName, $secret
Invoke-Command -Session $serverSession -ScriptBlock $setFolderScript -ArgumentList $folderName, $created.Outer
$report.Add(('round {0} at {1:HH:mm:ss}Z: subject {2}, outer group {3}' -f $round, [DateTime]::UtcNow, $created.User, $created.Outer))
foreach ($machine in $machines.Keys) {
$report.Add((' {0}: {1}' -f $machine, (Invoke-Command -Session $machines[$machine] -ScriptBlock $tokenScript -ArgumentList $userName, $domainName, $created.User, $created.Outer)))
}
# The order of the modules alternates, so that the module that asks first is not always the same.
$ordered = if ($round % 2) { $modules } else { @($modules)[($modules.Count - 1)..0] }
foreach ($module in $ordered) {
$answer = Invoke-Command -Session $clientSession -ArgumentList $stageName, $module.Label, $effectiveScript.ToString(), $folderName, $FileServer, $domainName, $netBiosName, $userName, $created.User -ScriptBlock $runEffectiveScript
$report.Add((' {0} on {1}: {2}' -f $module.Label, $Client, (@($answer) -join ' ')))
}
Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName
}
}
finally {
try { Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName } catch { $report.Add("cleanup on the domain controller failed: $($_.Exception.Message)") }
try { Invoke-Command -Session $serverSession -ArgumentList $folderName -ScriptBlock { param ($Folder) Remove-Item -LiteralPath (Join-Path -Path $env:SystemDrive -ChildPath $Folder) -Recurse -Force -ErrorAction SilentlyContinue } } catch { $report.Add("cleanup on the file server failed: $($_.Exception.Message)") }
try { Invoke-Command -Session $clientSession -ArgumentList $stageName -ScriptBlock { param ($Stage) Remove-Item -LiteralPath $Stage -Recurse -Force -ErrorAction SilentlyContinue } } catch { $report.Add("cleanup on the client failed: $($_.Exception.Message)") }
$report | Set-Content -LiteralPath $OutFile -Encoding utf8
Remove-PSSession -Session @($machines.Values) -ErrorAction SilentlyContinue
}
'done'
}

13
Tests/Lab/Acceptance/Run-MatrixLocalSuite.ps1

@ -97,6 +97,7 @@ foreach ($name in $targets) {
Write-Sequence "machine $name START"
$session = $null
$runCredential = $null
$resultsCopied = $false
try {
if ($name -eq 'LOCAL') {
$root = Join-Path -Path $env:TEMP -ChildPath "ntfs-localsuite-run-$Label"
@ -203,6 +204,7 @@ foreach ($name in $targets) {
if ($session) { Copy-Item -FromSession $session -Path (Join-Path -Path $root -ChildPath 'Results\*') -Destination $cellFolder -Recurse -Force }
else { Copy-Item -Path (Join-Path -Path $root -ChildPath 'Results\*') -Destination $cellFolder -Recurse -Force }
$resultsCopied = $true
foreach ($modeName in $modes) {
foreach ($editionName in $editions) {
$expected = ('{0}-{1}-{2}{3}' -f $Label, $name, $editionName, $(if ($modeName -eq 'Basic') { '-basic' } else { '' })).ToLowerInvariant()
@ -237,6 +239,17 @@ foreach ($name in $targets) {
Write-Sequence "machine ${name}: the scheduled tasks of this run could not be removed: $($_.Exception.Message)"
}
# The results are on the host, so the stage on the machine (the module, the tests, and the logs) is not needed any more. After an
# early stop it stays for the diagnosis.
if ($resultsCopied) {
try {
Invoke-Command -Session $session -ArgumentList $root -ScriptBlock { param ($Path) Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue }
}
catch {
Write-Sequence "machine ${name}: the stage $root could not be removed: $($_.Exception.Message)"
}
}
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}
}

18
Tests/Lab/Acceptance/Test-MatrixCleanup.ps1

@ -12,10 +12,10 @@ param (
# 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, 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
# behind (scheduled tasks, items in the 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.
# local group; the folders; the stage folders and scheduled tasks of the kit) and then reports like Verify.
& {
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
@ -67,9 +67,9 @@ 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
# What the suite runs and the probes of the kit leave behind: scheduled tasks, items in the 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
Stages = @('C:\NtfsMatrixLocal', 'C:\NtfsMatrixProbe' | Where-Object -FilterScript { Test-Path -LiteralPath $_ } | ForEach-Object -Process { Get-ChildItem -LiteralPath $_ -Force -ErrorAction SilentlyContinue }).Count
Users = @(Get-LocalUser -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Name -like 'NtfsProbe*' }).Count
}
}
@ -100,6 +100,14 @@ param (
$messages.Add(('{0}: present after {1} attempt(s): {2}' -f $path, $attempt, (Test-Path -LiteralPath $path)))
}
# What the suite runner and the probes of the kit left in their stage folders, and their scheduled tasks
foreach ($stage in 'C:\NtfsMatrixLocal', 'C:\NtfsMatrixProbe') {
if (Test-Path -LiteralPath $stage) { Get-ChildItem -LiteralPath $stage -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }
}
Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.TaskName -like 'NtfsMatrix*' } | ForEach-Object -Process { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -ErrorAction SilentlyContinue }
$messages.Add('stage folders and scheduled tasks of the kit removed')
$messages
}
@ -111,7 +119,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
' residue: scheduled tasks={0} stage items={1} probe users={2}' -f $state.Tasks, $state.Stages, $state.Users
}
}

Loading…
Cancel
Save