<#
. SYNOPSIS
Runs the live tests of NTFSSecurity in an AutomatedLab lab against versions of the module .
. DESCRIPTION
Prepares what NTFSSecurity . Live . Tests . ps1 needs in a lab with a domain controller , a file server , and a client of
one domain , and runs the tests for each module version and PowerShell edition : on the client as the accounts of the
roles Delegate , ServerAdmin , and Admin , then on the file server in the role Server . Each version runs in its own
process . README . md describes the cases ; the results and a summary go to a new folder in OutputPath .
The script changes only the lab : the organizational unit NTFSSecurityLive with the accounts and groups of the
tests , the local group NtfsLiveLocal and members of Administrators on the file server , members of Administrators
and Remote Management Users on the client , the share NTFSSecurityLive , and the folders C: \ NTFSSecurityLive and
C: \ NTFSSecurityLab . -RemoveFixture removes them again . The passwords of the accounts exist only in this process .
. PARAMETER LabName
The name of the AutomatedLab lab .
. PARAMETER DomainController
The domain controller that manages the accounts of the tests .
. PARAMETER FileServer
The file server with the share . Its domain must be the domain of the domain controller .
. PARAMETER Client
The computer that runs the tests . Its domain must be the domain of the domain controller .
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
. PARAMETER ForeignDomainController
Domain controllers of other domains or forests , one per domain . The script creates the account NtfsLiveForeign in
each of their domains , and the tests grant it access to share folders by SID and by name . The domains need a trust
with the domain of the file server . Pass an empty array to leave these tests out .
. PARAMETER Version
The versions of NTFSSecurity on the PowerShell Gallery to test , such as 5 . 0 . 0 -rc4 .
. PARAMETER ModulePath
A folder with a build of the module , such as NTFSSecurity \ bin \ Release , to test as the version 'local' .
. PARAMETER Edition
The PowerShell editions to test in : Desktop ( Windows PowerShell 5 . 1 ) and Core ( PowerShell 7 ) .
. PARAMETER OutputPath
The folder for the downloaded packages and the results .
. PARAMETER RemoveFixture
Removes everything the script added to the lab , and runs no test .
. EXAMPLE
. \ Tests \ Lab \ Invoke-NTFSSecurityLabTest . ps1 -Version 5 . 0 . 0 -rc2 , 5 . 0 . 0 -rc4 -Confirm : $false
Runs the live tests against 5 . 0 . 0 -rc2 and 5 . 0 . 0 -rc4 in both PowerShell editions .
. EXAMPLE
. \ Tests \ Lab \ Invoke-NTFSSecurityLabTest . ps1 -RemoveFixture
Removes the accounts , the share , and the folders of the live tests from the lab .
. NOTES
Run it in an elevated Windows PowerShell 5 . 1 session on the Hyper-V host of the lab , with AutomatedLab .
#>
[ CmdletBinding ( SupportsShouldProcess , ConfirmImpact = 'High' , DefaultParameterSetName = 'Test' ) ]
param (
[ Parameter ( ) ]
[ ValidateNotNullOrEmpty ( ) ]
[string]
$LabName = 'WindowsAccessControlLab' ,
[ Parameter ( ) ]
[ ValidateNotNullOrEmpty ( ) ]
[string]
$DomainController = 'F1ADC1' ,
[ Parameter ( ) ]
[ ValidateNotNullOrEmpty ( ) ]
[string]
$FileServer = 'F1AFile2' ,
[ Parameter ( ) ]
[ ValidateNotNullOrEmpty ( ) ]
[string]
$Client = 'F1AFile1' ,
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
[ Parameter ( ) ]
[ AllowEmptyCollection ( ) ]
[string[]]
$ForeignDomainController = @ ( 'F1BDC1' , 'F2DC1' , 'F3DC1' ) ,
[ Parameter ( ParameterSetName = 'Test' ) ]
[ ValidatePattern ( '^\d+\.\d+\.\d+(-[0-9A-Za-z]+)?$' ) ]
[string[]]
$Version = @ ( '5.0.0-rc2' , '5.0.0-rc4' ) ,
[ Parameter ( ParameterSetName = 'Test' ) ]
[ ValidateScript ( { Test-Path -LiteralPath ( Join-Path -Path $_ -ChildPath 'NTFSSecurity.psd1' ) -PathType Leaf } ) ]
[string]
$ModulePath ,
[ Parameter ( ParameterSetName = 'Test' ) ]
[ ValidateSet ( 'Desktop' , 'Core' ) ]
[string[]]
$Edition = @ ( 'Desktop' , 'Core' ) ,
[ Parameter ( ) ]
[ ValidateNotNullOrEmpty ( ) ]
[string]
$OutputPath = ( Join-Path -Path $env:TEMP -ChildPath 'NTFSSecurityLab' ) ,
[ Parameter ( Mandatory , ParameterSetName = 'Remove' ) ]
[switch]
$RemoveFixture
)
$ErrorActionPreference = 'Stop'
$currentPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList (
[System.Security.Principal.WindowsIdentity] :: GetCurrent ( )
)
if ( -not $currentPrincipal . IsInRole ( [System.Security.Principal.WindowsBuiltInRole] :: Administrator ) ) {
throw 'This script must run in an elevated (Run as Administrator) PowerShell session.'
}
$shareName = 'NTFSSecurityLive'
$shareLocalPath = 'C:\NTFSSecurityLive'
$payloadPath = 'C:\NTFSSecurityLab'
$organizationalUnitName = 'NTFSSecurityLive'
$roleAccounts = [ordered] @ {
Delegate = 'NtfsLiveDelegate'
ServerAdmin = 'NtfsLiveServerAdmin'
Admin = 'NtfsLiveAdmin'
}
$subjectAccount = 'NtfsLiveSubject'
$orphanAccount = 'NtfsLiveOrphan'
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$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'
$groupMembers = @ {
NtfsLiveDelegates = @ ( 'NtfsLiveDelegate' )
NtfsLiveInner = @ ( 'NtfsLiveSubject' )
NtfsLiveOuter = @ ( 'NtfsLiveInner' )
}
# A name that no DNS server resolves (RFC 2606)
$unreachableServerName = 'ntfssecurity-live.invalid'
# ReadAndExecute through the nested domain groups, Write through the local group of the file server, with Synchronize
$expectedFileServerRights = 0x1201BFL
$expectedClientRights = 0x1200A9L
$testFiles = 'NTFSSecurity.Live.Tests.ps1' , 'NTFSSecurity.LabHelpers.ps1' , 'Start-NTFSSecurityLiveTest.ps1' |
ForEach-Object -Process { Join-Path -Path $PSScriptRoot -ChildPath $_ }
function Write-LabProgress {
param ( [string] $Message )
Write-Information -MessageData ( '[{0:yyyy-MM-dd HH:mm:ss}] {1}' -f ( Get-Date ) , $Message ) -InformationAction Continue
}
function New-LabPassword {
<#
. SYNOPSIS
Returns a random password that meets the complexity rules of a domain .
#>
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseShouldProcessForStateChangingFunctions' , '' , Justification = 'Creates a string in memory only.'
) ]
[ CmdletBinding ( ) ]
[ OutputType ( [securestring] ) ]
param ( )
$bytes = New-Object -TypeName 'byte[]' -ArgumentList 24
$generator = [System.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.
$password = New-Object -TypeName 'System.Security.SecureString'
foreach ( $character in ( [Convert] :: ToBase64String ( $bytes ) + '!a1Z' ) . ToCharArray ( ) ) {
$password . AppendChar ( $character )
}
$password . MakeReadOnly ( )
$password
}
function Get-NTFSSecurityPackage {
<#
. SYNOPSIS
Downloads a version of NTFSSecurity from the PowerShell Gallery , checks its hash , and extracts the module .
#>
[ CmdletBinding ( ) ]
[ OutputType ( [pscustomobject] ) ]
param (
[ Parameter ( Mandatory ) ]
[string]
$Version ,
[ Parameter ( Mandatory ) ]
[string]
$Destination
)
$entry = Invoke-RestMethod -Uri ( " https://www.powershellgallery.com/api/v2/Packages(Id='NTFSSecurity',Version='{0}') " -f $Version )
$expectedHash = [string] $entry . entry . properties . PackageHash
if ( $entry . entry . properties . PackageHashAlgorithm -ne 'SHA512' -or -not $expectedHash ) {
throw " The PowerShell Gallery has no SHA512 hash for NTFSSecurity $ Version. "
}
$package = Join-Path -Path $Destination -ChildPath " NTFSSecurity. $ Version.nupkg "
if ( -not ( Test-Path -LiteralPath $package ) ) {
Invoke-WebRequest -Uri " https://www.powershellgallery.com/api/v2/package/NTFSSecurity/ $ Version " -OutFile $package -UseBasicParsing
}
$sha512 = [System.Security.Cryptography.SHA512] :: Create ( )
try {
$actualHash = [Convert] :: ToBase64String ( $sha512 . ComputeHash ( [System.IO.File] :: ReadAllBytes ( $package ) ) )
}
finally {
$sha512 . Dispose ( )
}
if ( $actualHash -ne $expectedHash ) {
Remove-Item -LiteralPath $package
throw " The package of NTFSSecurity $ Version doesn't have the hash that the PowerShell Gallery publishes. "
}
# Expand-Archive of Windows PowerShell rejects the extension .nupkg.
$folder = Join-Path -Path $Destination -ChildPath $Version
$moduleFolder = Join-Path -Path $folder -ChildPath 'NTFSSecurity'
if ( Test-Path -LiteralPath $folder ) {
Remove-Item -LiteralPath $folder -Recurse -Force
}
Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
[System.IO.Compression.ZipFile] :: ExtractToDirectory ( $package , $moduleFolder )
foreach ( $name in '_rels' , 'package' , '[Content_Types].xml' , 'NTFSSecurity.nuspec' ) {
$item = Join-Path -Path $moduleFolder -ChildPath $name
if ( Test-Path -LiteralPath $item ) {
Remove-Item -LiteralPath $item -Recurse -Force
}
}
$moduleVersion = Get-LabModuleVersion -Path $moduleFolder
if ( $moduleVersion -ne $Version ) {
throw " The package of NTFSSecurity $ Version contains the version $ moduleVersion. "
}
[pscustomobject] @ {
Label = $Version
Folder = $folder
ModuleVersion = $moduleVersion
}
}
function Get-LabModuleVersion {
<#
. SYNOPSIS
Returns the version of the module in a folder , with the prerelease label , from its manifest .
#>
[ CmdletBinding ( ) ]
[ OutputType ( [string] ) ]
param (
[ Parameter ( Mandatory ) ]
[string]
$Path
)
$manifest = Import-PowerShellDataFile -LiteralPath ( Join-Path -Path $Path -ChildPath 'NTFSSecurity.psd1' )
$moduleVersion = $manifest . ModuleVersion
if ( $manifest . PrivateData . PSData . Prerelease ) {
$moduleVersion = '{0}-{1}' -f $moduleVersion , $manifest . PrivateData . PSData . Prerelease
}
$moduleVersion
}
function ConvertFrom-LabTestResult {
<#
. SYNOPSIS
Returns the tests of a result file that Start-NTFSSecurityLiveTest . ps1 wrote , with their result and message .
#>
[ CmdletBinding ( ) ]
[ OutputType ( [pscustomobject] ) ]
param (
[ Parameter ( Mandatory ) ]
[string]
$Json
)
# Windows PowerShell returns a JSON array as one object, which foreach enumerates.
$tests = ConvertFrom-Json -InputObject $Json
foreach ( $test in $tests ) {
[pscustomobject] @ {
Name = $test . Name
Result = $test . Result
Message = [string] $test . Message
}
}
}
#region Remote script blocks
# 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 = {
param ( $OrganizationalUnitName , [hashtable] $Password , [hashtable] $GroupMember )
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$server = $domain . PDCEmulator
$path = 'OU={0},{1}' -f $OrganizationalUnitName , $domain . DistinguishedName
if ( -not ( Get-ADOrganizationalUnit -LDAPFilter " (ou= $ OrganizationalUnitName) " -SearchBase $domain . DistinguishedName -SearchScope OneLevel -Server $server ) ) {
New-ADOrganizationalUnit -Name $OrganizationalUnitName -Path $domain . DistinguishedName -ProtectedFromAccidentalDeletion $false -Server $server
}
foreach ( $name in $Password . Keys ) {
$user = Get-ADUser -LDAPFilter " (sAMAccountName= $ name) " -Server $server
if ( $user -and $user . DistinguishedName -notlike " *, $ path " ) {
throw " The account ' $ name' exists outside ' $ path'. "
}
if ( $user ) {
Set-ADAccountPassword -Identity $user -Reset -NewPassword $Password [ $name ] -Server $server
Enable-ADAccount -Identity $user -Server $server
}
else {
New-ADUser -Name $name -SamAccountName $name -UserPrincipalName " $ name@ $( $domain . DNSRoot ) " -Path $path -AccountPassword $Password [ $name ] -Enabled $true -PasswordNeverExpires $true -Server $server
}
}
# All groups exist before the memberships are set, because a group can be a member of another one.
$groups = @ { }
foreach ( $name in $GroupMember . Keys ) {
$group = Get-ADGroup -LDAPFilter " (sAMAccountName= $ name) " -Server $server
if ( $group -and $group . DistinguishedName -notlike " *, $ path " ) {
throw " The group ' $ name' exists outside ' $ path'. "
}
if ( -not $group ) {
$group = New-ADGroup -Name $name -SamAccountName $name -GroupScope Global -GroupCategory Security -Path $path -Server $server -PassThru
}
$groups [ $name ] = $group
}
foreach ( $name in $GroupMember . Keys ) {
$group = $groups [ $name ]
$current = @ ( Get-ADGroupMember -Identity $group -Server $server | ForEach-Object -Process { $_ . SamAccountName } )
$wanted = @ ( $GroupMember [ $name ] )
$missing = @ ( $wanted | Where-Object -FilterScript { $_ -notin $current } )
$extra = @ ( $current | Where-Object -FilterScript { $_ -notin $wanted } )
if ( $missing ) {
Add-ADGroupMember -Identity $group -Members $missing -Server $server
}
if ( $extra ) {
Remove-ADGroupMember -Identity $group -Members $extra -Server $server -Confirm : $false
}
}
# The other domain controllers replicate on their own schedule, and the computers of the domain may ask them.
$partners = @ ( Get-ADDomainController -Filter * -Server $server | Where-Object -FilterScript { $_ . HostName -ne $server } )
foreach ( $partner in $partners ) {
foreach ( $object in Get-ADObject -SearchBase $path -Filter * -Server $server ) {
Sync-ADObject -Object $object . DistinguishedName -Source $server -Destination $partner . HostName
}
}
$sids = @ { }
foreach ( $name in @ ( $Password . Keys ) + @ ( $GroupMember . Keys ) ) {
$sids [ $name ] = ( Get-ADObject -LDAPFilter " (sAMAccountName= $ name) " -Properties objectSid -Server $server ) . objectSid . Value
}
$current = [System.Security.Principal.WindowsIdentity] :: GetCurrent ( )
[pscustomobject] @ {
DomainName = $domain . DNSRoot
NetBiosName = $domain . NetBIOSName
Sids = $sids
InstallName = $current . Name
InstallSid = $current . User . Value
}
}
# Runs on the domain controller: creates the account whose entry becomes orphaned in a run.
$newOrphanScript = {
param ( $OrganizationalUnitName , $Name , [securestring] $Password )
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$server = $domain . PDCEmulator
$path = 'OU={0},{1}' -f $OrganizationalUnitName , $domain . DistinguishedName
# A run that stopped early may have left the account.
Get-ADUser -LDAPFilter " (sAMAccountName= $ Name) " -SearchBase $path -Server $server | Remove-ADUser -Server $server -Confirm : $false
$user = New-ADUser -Name $Name -SamAccountName $Name -Path $path -AccountPassword $Password -Enabled $false -Server $server -PassThru
[pscustomobject] @ {
Sid = $user . SID . Value
Guid = $user . ObjectGUID . Guid
}
}
# Runs on the domain controller: deletes the account and waits until no domain controller of the domain knows it.
# Sync-ADObject can't push a deleted object, so the other domain controllers pull the domain partition.
$removeOrphanScript = {
param ( $Guid )
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$server = $domain . PDCEmulator
Remove-ADUser -Identity $Guid -Server $server -Confirm : $false
foreach ( $partner in @ ( Get-ADDomainController -Filter * -Server $server | Where-Object -FilterScript { $_ . HostName -ne $server } ) ) {
$deadline = ( Get-Date ) . AddMinutes ( 3 )
while ( Get-ADObject -Filter " ObjectGUID -eq ' $ Guid' " -Server $partner . HostName ) {
if ( ( Get-Date ) -gt $deadline ) {
throw " The domain controller $( $partner . HostName ) still knows the deleted account after 3 minutes. "
}
$null = & repadmin . exe / replicate $partner . HostName $server $domain . DistinguishedName 2 > & 1
Start-Sleep -Seconds 10
}
}
}
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# Runs on a domain controller of another domain or forest: the account that the tests of case 9 grant access to.
$foreignAccountScript = {
param ( $OrganizationalUnitName , $Name , [securestring] $Password )
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$server = $domain . PDCEmulator
$path = 'OU={0},{1}' -f $OrganizationalUnitName , $domain . DistinguishedName
if ( -not ( Get-ADOrganizationalUnit -LDAPFilter " (ou= $ OrganizationalUnitName) " -SearchBase $domain . DistinguishedName -SearchScope OneLevel -Server $server ) ) {
New-ADOrganizationalUnit -Name $OrganizationalUnitName -Path $domain . DistinguishedName -ProtectedFromAccidentalDeletion $false -Server $server
}
$user = Get-ADUser -LDAPFilter " (sAMAccountName= $ Name) " -Server $server
if ( $user -and $user . DistinguishedName -notlike " *, $ path " ) {
throw " The account ' $ Name' exists outside ' $ path'. "
}
if ( $user ) {
Set-ADAccountPassword -Identity $user -Reset -NewPassword $Password -Server $server
Enable-ADAccount -Identity $user -Server $server
}
else {
$user = New-ADUser -Name $Name -SamAccountName $Name -UserPrincipalName " $ Name@ $( $domain . DNSRoot ) " -Path $path -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true -Server $server -PassThru
}
[pscustomobject] @ {
DomainName = $domain . DNSRoot
Name = '{0}\{1}' -f $domain . NetBIOSName , $Name
UserPrincipalName = '{0}@{1}' -f $Name , $domain . DNSRoot
Sid = $user . SID . Value
}
}
# Runs on the file server once: the local group, the members of Administrators, the share, and the tools folder.
$fileServerSetupScript = {
param ( $ShareName , $ShareLocalPath , $PayloadPath , $LocalGroupName , $SubjectSid , $AdministratorSid , $DelegatesSid )
$ErrorActionPreference = 'Stop'
if ( -not ( Get-LocalGroup -Name $LocalGroupName -ErrorAction SilentlyContinue ) ) {
$null = New-LocalGroup -Name $LocalGroupName -Description 'NTFSSecurity live tests'
}
if ( $SubjectSid -notin @ ( Get-LocalGroupMember -Name $LocalGroupName | ForEach-Object -Process { $_ . SID . Value } ) ) {
Add-LocalGroupMember -Name $LocalGroupName -Member $SubjectSid
}
foreach ( $sid in $AdministratorSid ) {
if ( $sid -notin @ ( Get-LocalGroupMember -SID 'S-1-5-32-544' | ForEach-Object -Process { $_ . SID . Value } ) ) {
Add-LocalGroupMember -SID 'S-1-5-32-544' -Member $sid
}
}
if ( -not ( Test-Path -LiteralPath $ShareLocalPath ) ) {
$null = New-Item -ItemType Directory -Path $ShareLocalPath
}
# The folders of earlier runs, also those with paths longer than 260 characters, which PowerShell 7 removes.
$command = '$ErrorActionPreference = ''Stop''; Get-ChildItem -LiteralPath ''{0}'' -Force | Remove-Item -Recurse -Force' -f $ShareLocalPath
$output = & ( Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe' ) -NoProfile -NonInteractive -EncodedCommand ( [Convert] :: ToBase64String ( [System.Text.Encoding] :: Unicode . GetBytes ( $command ) ) ) 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
throw " The folders of earlier runs could not be removed: $ output "
}
# Administrators and the system own the share; the delegated group may read it, and the folders of the cases grant
# what each case needs.
$administrators = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-544'
$acl = New-Object -TypeName 'System.Security.AccessControl.DirectorySecurity'
$acl . SetOwner ( $administrators )
$acl . SetAccessRuleProtection ( $true , $false )
foreach ( $entry in @ (
@ { Sid = 'S-1-5-32-544' ; Rights = 'FullControl' }
@ { Sid = 'S-1-5-18' ; Rights = 'FullControl' }
@ { Sid = $DelegatesSid ; Rights = 'ReadAndExecute' }
) ) {
$identity = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $entry . Sid
$acl . AddAccessRule ( ( New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList $identity , $entry . Rights , 'ContainerInherit, ObjectInherit' , 'None' , 'Allow' ) )
}
Set-Acl -LiteralPath $ShareLocalPath -AclObject $acl
if ( -not ( Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue ) ) {
$null = New-SmbShare -Name $ShareName -Path $ShareLocalPath -FullAccess 'Everyone' -Description 'NTFSSecurity live tests'
}
foreach ( $folder in 'Tests' , 'Configuration' ) {
$null = New-Item -ItemType Directory -Path ( Join-Path -Path $PayloadPath -ChildPath $folder ) -Force
}
( Get-LocalGroup -Name $LocalGroupName ) . SID . Value
}
# Runs on the client once: the members of Administrators and Remote Management Users, and an empty payload folder.
$clientSetupScript = {
param ( $PayloadPath , $AdministratorSid , $RemoteUserSid )
$ErrorActionPreference = 'Stop'
foreach ( $membership in @ (
@ { Group = 'S-1-5-32-544' ; Members = $AdministratorSid }
@ { Group = 'S-1-5-32-580' ; Members = $RemoteUserSid }
) ) {
foreach ( $sid in $membership . Members ) {
if ( $sid -notin @ ( Get-LocalGroupMember -SID $membership . Group | ForEach-Object -Process { $_ . SID . Value } ) ) {
Add-LocalGroupMember -SID $membership . Group -Member $sid
}
}
}
if ( Test-Path -LiteralPath $PayloadPath ) {
Remove-Item -LiteralPath $PayloadPath -Recurse -Force
}
foreach ( $folder in 'Modules' , 'Tests' , 'Configuration' ) {
$null = New-Item -ItemType Directory -Path ( Join-Path -Path $PayloadPath -ChildPath $folder ) -Force
}
}
# Runs on the file server for each run: creates the folders of the cases below the share and returns what the
# configuration needs. Each case and operation gets its own folder, so that the tests don't depend on each other.
$fixtureScript = {
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
param ( $HelperScript , $ShareLocalPath , $RunId , $Sid , $SubjectPrincipalName , $LongPathSegment , $ForeignAccount )
$ErrorActionPreference = 'Stop'
. ( [scriptblock] :: Create ( $HelperScript ) )
$root = Join-Path -Path $ShareLocalPath -ChildPath $RunId
if ( Test-Path -LiteralPath $root ) {
throw " The folder ' $ root' exists already. "
}
$administrators = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-544'
function New-AccessRule {
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseShouldProcessForStateChangingFunctions' , '' , Justification = 'Creates an object in memory only.'
) ]
param ( [string] $Sid , [string] $Rights )
$identity = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $Sid
New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList $identity , $Rights , 'ContainerInherit, ObjectInherit' , 'None' , 'Allow'
}
function New-FixtureFolder {
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseShouldProcessForStateChangingFunctions' , '' , Justification = 'Runs in the fixture that the script confirmed.'
) ]
param ( [string] $RelativePath , [object[]] $AccessRule = @ ( ) , [switch] $Protected , [switch] $RemoveInherited ,
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
[switch] $Audit , [switch] $LegacyDacl , [switch] $InheritableAudit , [switch] $ProtectedAudit )
$path = Join-Path -Path $root -ChildPath $RelativePath
$null = New-Item -ItemType Directory -Path $path -Force
$acl = Get-Acl -LiteralPath $path
$acl . SetOwner ( $administrators )
if ( $Protected ) {
$acl . SetAccessRuleProtection ( $true , -not $RemoveInherited )
}
foreach ( $rule in $AccessRule ) {
$acl . AddAccessRule ( $rule )
}
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# Not Set-Acl: in Windows PowerShell, it also writes an empty, protected SACL, which drops the audit entries
# that the folder inherits. SetAccessControl writes only the sections that changed.
[System.IO.Directory] :: SetAccessControl ( $path , $acl )
if ( $Audit -or $InheritableAudit -or $ProtectedAudit ) {
$auditAcl = Get-Acl -LiteralPath $path -Audit
$everyone = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0'
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
if ( $Audit ) {
$auditAcl . AddAuditRule ( ( New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList $everyone , 'Delete' , 'None' , 'None' , 'Success' ) )
}
# An entry that the subfolders created afterwards inherit
if ( $InheritableAudit ) {
$auditAcl . AddAuditRule ( ( New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList $everyone , 'Delete' , 'ContainerInherit, ObjectInherit' , 'None' , 'Failure' ) )
}
if ( $ProtectedAudit ) {
$auditAcl . SetAuditRuleProtection ( $true , $false )
}
[System.IO.Directory] :: SetAccessControl ( $path , $auditAcl )
}
# Set-Acl adds the auto-inherit flag, so the DACL is stored again without it at the end.
if ( $LegacyDacl ) {
Set-LabLegacyDacl -Path $path -Confirm : $false
}
if ( ( Get-LabSecurityDescriptor -Path $path ) . Owner . Value -ne 'S-1-5-32-544' ) {
throw " Administrators don't own ' $ path'. "
}
if ( ( Test-LabDaclAutoInherited -Path $path ) -eq [bool] $LegacyDacl ) {
throw " The DACL of ' $ path' has the wrong auto-inherit flag. "
}
$path
}
$null = New-Item -ItemType Directory -Path $root
$delegatesFullControl = New-AccessRule -Sid $Sid . NtfsLiveDelegates -Rights 'FullControl'
# Case 1 (#34): the delegated group has Full Control on folders that Administrators own.
foreach ( $variant in 'LegacyDacl' , 'AutoInheritedDacl' ) {
foreach ( $operation in 'AddAccess' , 'RemoveAccess' , 'ClearAccess' , 'DisableInheritance' , 'EnableInheritance' ,
'SetInheritance' , 'SetSecurityDescriptor' ) {
$rules = @ ( $delegatesFullControl )
if ( $operation -eq 'RemoveAccess' ) {
$rules + = New-AccessRule -Sid 'S-1-1-0' -Rights 'ReadAndExecute'
}
$null = New-FixtureFolder -RelativePath " Case1\ $ variant\ $ operation " -AccessRule $rules -Protected : ( $operation -eq 'EnableInheritance' ) -LegacyDacl : ( $variant -eq 'LegacyDacl' )
}
}
# Case 2: the same Full Control, and an audit entry on the folders whose entry the tests read or remove.
foreach ( $auditRole in 'Admin' , 'ServerAdmin' , 'Delegate' ) {
$null = New-FixtureFolder -RelativePath " Case2\ $ auditRole\GetAudit " -AccessRule $delegatesFullControl -Audit
$null = New-FixtureFolder -RelativePath " Case2\ $ auditRole\AddAudit " -AccessRule $delegatesFullControl
$null = New-FixtureFolder -RelativePath " Case2\ $ auditRole\RemoveAudit " -AccessRule $delegatesFullControl -Audit
}
# Case 3: only these entries, so that the expected rights are known.
$effectivePath = New-FixtureFolder -RelativePath 'Case3\EffectiveAccess' -Protected -RemoveInherited -AccessRule @ (
New-AccessRule -Sid 'S-1-5-32-544' -Rights 'FullControl'
New-AccessRule -Sid 'S-1-5-18' -Rights 'FullControl'
New-AccessRule -Sid $Sid . NtfsLiveOuter -Rights 'ReadAndExecute'
New-AccessRule -Sid $Sid . LocalGroup -Rights 'Write'
)
$effectiveDescriptor = Get-LabSecurityDescriptor -Path $effectivePath
$fileServerRights = Get-LabGrantedRight -Descriptor $effectiveDescriptor -Sid @ ( Get-LabTokenSid -UserPrincipalName $SubjectPrincipalName )
# Case 4: an entry for the account that the domain controller deletes next, and a file that inherits it.
$orphanPath = New-FixtureFolder -RelativePath 'Case4\OrphanedAccess' -AccessRule ( New-AccessRule -Sid $Sid . Orphan -Rights 'Modify' )
Set-Content -LiteralPath ( Join-Path -Path $orphanPath -ChildPath 'File.txt' ) -Value 'Orphan'
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$orphanAuditPath = New-FixtureFolder -RelativePath 'Case4\OrphanedAudit' -AccessRule $delegatesFullControl
$orphanAuditAcl = Get-Acl -LiteralPath $orphanAuditPath -Audit
$orphanIdentity = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $Sid . Orphan
$orphanAuditAcl . AddAuditRule ( ( New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList $orphanIdentity , 'Delete' , 'None' , 'None' , 'Success' ) )
[System.IO.Directory] :: SetAccessControl ( $orphanAuditPath , $orphanAuditAcl )
# A file whose path on the share is longer than 260 characters, created by PowerShell 7, which handles such paths.
$longPath = New-FixtureFolder -RelativePath 'LongPath'
$longRelativePath = ( $LongPathSegment -join '\' ) + '\File.txt'
$command = '$ErrorActionPreference = ''Stop''; $file = ''{0}''; $null = New-Item -ItemType Directory -Path (Split-Path -Path $file -Parent) -Force; Set-Content -LiteralPath $file -Value ''LongPath''' -f ( Join-Path -Path $longPath -ChildPath $longRelativePath )
$output = & ( Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe' ) -NoProfile -NonInteractive -EncodedCommand ( [Convert] :: ToBase64String ( [System.Text.Encoding] :: Unicode . GetBytes ( $command ) ) ) 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
throw " The long path could not be created: $ output "
}
$whatIfPath = New-FixtureFolder -RelativePath 'WhatIf'
Set-Content -LiteralPath ( Join-Path -Path $whatIfPath -ChildPath 'Source.txt' ) -Value 'Source' -NoNewline
Set-Content -LiteralPath ( Join-Path -Path $whatIfPath -ChildPath 'Destination.txt' ) -Value 'Destination' -NoNewline
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# Case 5: the owner cmdlets, and case 6: the audit inheritance cmdlets and Clear-NTFSAudit below a folder whose
# audit entry the subfolders inherit, on folders that Administrators own and the delegated group fully controls.
foreach ( $role in 'Admin' , 'ServerAdmin' , 'Delegate' ) {
foreach ( $operation in 'GetOwner' , 'TakeOwnership' , 'AssignOwner' ) {
$null = New-FixtureFolder -RelativePath " Case5\ $ role\ $ operation " -AccessRule $delegatesFullControl
}
$null = New-FixtureFolder -RelativePath " Case6\ $ role " -AccessRule $delegatesFullControl -InheritableAudit
$null = New-FixtureFolder -RelativePath " Case6\ $ role\DisableAuditInheritance "
$null = New-FixtureFolder -RelativePath " Case6\ $ role\EnableAuditInheritance " -ProtectedAudit
$null = New-FixtureFolder -RelativePath " Case6\ $ role\ClearAudit " -Audit
$null = New-FixtureFolder -RelativePath " Case6\ $ role\GetInheritance " -Protected
}
# Case 7: the item cmdlets in a folder that the delegated group fully controls.
$itemsPath = New-FixtureFolder -RelativePath 'Case7\Items' -AccessRule $delegatesFullControl
foreach ( $name in 'Source' , 'Move' , 'Remove' ) {
Set-Content -LiteralPath ( Join-Path -Path $itemsPath -ChildPath " $ name.txt " ) -Value $name -NoNewline
}
$null = New-Item -ItemType Directory -Path ( Join-Path -Path $itemsPath -ChildPath 'Folder' )
Set-Content -LiteralPath ( Join-Path -Path $itemsPath -ChildPath 'Folder\File.txt' ) -Value 'File' -NoNewline
# Case 8: the link cmdlets.
$linksPath = New-FixtureFolder -RelativePath 'Case8\Links' -AccessRule $delegatesFullControl
Set-Content -LiteralPath ( Join-Path -Path $linksPath -ChildPath 'Target.txt' ) -Value 'Target' -NoNewline
$null = New-Item -ItemType Directory -Path ( Join-Path -Path $linksPath -ChildPath 'TargetFolder' )
# Case 9: a subfolder with an entry of its own for Get-NTFSSimpleAccess, and the accounts of other domains.
$null = New-FixtureFolder -RelativePath 'Case9\Simple' -AccessRule $delegatesFullControl
$null = New-FixtureFolder -RelativePath 'Case9\Simple\Child' -AccessRule ( New-AccessRule -Sid $Sid . Subject -Rights 'Modify' )
$foreignPath = New-FixtureFolder -RelativePath 'Case9\Foreign' -AccessRule @ (
foreach ( $account in $ForeignAccount ) {
New-AccessRule -Sid $account . Sid -Rights $account . Rights
}
)
$null = New-FixtureFolder -RelativePath 'Case9\ForeignAdd' -AccessRule $delegatesFullControl
$null = New-FixtureFolder -RelativePath 'Case9\ForeignRemove' -AccessRule @ (
foreach ( $account in $ForeignAccount ) {
New-AccessRule -Sid $account . Sid -Rights 'ReadAndExecute'
}
)
# The rights that the file server's own token of each foreign account gets on the folder, like case 3. A token
# that the file server can't create is reported as -1, which fails only the effective-access test of the account.
$foreignDescriptor = Get-LabSecurityDescriptor -Path $foreignPath
$foreignEffectiveRights = @ { }
foreach ( $account in $ForeignAccount ) {
try {
$foreignEffectiveRights [ $account . Sid ] = Get-LabGrantedRight -Descriptor $foreignDescriptor -Sid @ ( Get-LabTokenSid -UserPrincipalName $account . UserPrincipalName )
}
catch {
$foreignEffectiveRights [ $account . Sid ] = - 1L
}
}
[pscustomobject] @ {
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
ServerPath = $root
FileServerRights = $fileServerRights
EffectiveAccessSddl = $effectiveDescriptor . GetSddlForm ( 'All' )
LongPath = $longRelativePath
ForeignEffectiveRights = $foreignEffectiveRights
}
}
# Runs on the client for each run: the rights that the client's own token for the account grants on the folder of
# case 3, and whether the client still resolves the SID of the deleted account.
$clientOracleScript = {
param ( $HelperScript , $EffectiveAccessSddl , $SubjectPrincipalName , $OrphanSid )
$ErrorActionPreference = 'Stop'
. ( [scriptblock] :: Create ( $HelperScript ) )
$descriptor = New-Object -TypeName 'System.Security.AccessControl.RawSecurityDescriptor' -ArgumentList $EffectiveAccessSddl
$orphanResolved = $true
try {
$null = ( New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $OrphanSid ) . Translate ( [System.Security.Principal.NTAccount] )
}
catch [System.Security.Principal.IdentityNotMappedException] {
$orphanResolved = $false
}
[pscustomobject] @ {
ClientRights = Get-LabGrantedRight -Descriptor $descriptor -Sid @ ( Get-LabTokenSid -UserPrincipalName $SubjectPrincipalName )
OrphanResolved = $orphanResolved
}
}
# Runs on the client and the file server: writes the configuration of a run.
$writeConfigurationScript = {
param ( $Path , $Json )
$ErrorActionPreference = 'Stop'
Set-Content -LiteralPath $Path -Value $Json -Encoding UTF8
}
# Runs on the client as the account of a role, or on the file server: runs the tests of the role in a new process.
$runScript = {
param ( $Edition , $TestPath , $ModulePath , $ConfigurationPath , $Role )
$executable = if ( $Edition -eq 'Core' ) {
Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe'
}
else {
Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
}
$resultPath = Join-Path -Path ( [System.IO.Path] :: GetTempPath ( ) ) -ChildPath ( 'NTFSSecurityLive-{0}.json' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) )
$arguments = @ (
'-NoProfile' , '-NonInteractive' , '-ExecutionPolicy' , 'Bypass'
'-File' , ( Join-Path -Path $TestPath -ChildPath 'Start-NTFSSecurityLiveTest.ps1' )
'-ConfigurationPath' , $ConfigurationPath , '-Role' , $Role , '-ResultPath' , $resultPath
)
if ( $ModulePath ) {
$arguments + = '-ModulePath' , $ModulePath
}
$output = & $executable @arguments 2 > & 1 | ForEach-Object -Process { " $ _ " }
$exitCode = $LASTEXITCODE
$result = if ( Test-Path -LiteralPath $resultPath ) { Get-Content -LiteralPath $resultPath -Raw }
Remove-Item -LiteralPath $resultPath -ErrorAction SilentlyContinue
[pscustomobject] @ {
Account = [System.Security.Principal.WindowsIdentity] :: GetCurrent ( ) . Name
ExitCode = $exitCode
Output = $output -join [Environment] :: NewLine
Result = $result
}
}
# Runs on the file server after the tests of a run: the stored owner, group, and DACL, and the SACL of each folder.
$stateScript = {
param ( $HelperScript , $Root )
$ErrorActionPreference = 'Stop'
. ( [scriptblock] :: Create ( $HelperScript ) )
foreach ( $folder in Get-ChildItem -LiteralPath $Root -Directory -Recurse -Depth 2 ) {
[pscustomobject] @ {
Path = $folder . FullName . Substring ( $Root . Length + 1 )
Stored = ( Get-LabSecurityDescriptor -Path $folder . FullName ) . GetSddlForm ( 'All' )
Sacl = ( Get-Acl -LiteralPath $folder . FullName -Audit ) . GetSecurityDescriptorSddlForm ( 'Audit' )
}
}
}
# Runs on the domain controller: the SIDs of the accounts in the organizational unit, for -RemoveFixture.
$accountSidScript = {
param ( $OrganizationalUnitName )
$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 -Filter * -SearchBase $path -Server $domain . PDCEmulator | ForEach-Object -Process { $_ . SID . Value }
}
}
$removeFileServerScript = {
param ( $ShareName , $ShareLocalPath , $PayloadPath , $LocalGroupName , $Sid )
$ErrorActionPreference = 'Stop'
if ( Get-SmbShare -Name $ShareName -ErrorAction SilentlyContinue ) {
Remove-SmbShare -Name $ShareName -Force
}
foreach ( $path in $ShareLocalPath , $PayloadPath ) {
if ( Test-Path -LiteralPath $path ) {
$command = '$ErrorActionPreference = ''Stop''; Remove-Item -LiteralPath ''{0}'' -Recurse -Force' -f $path
$output = & ( Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe' ) -NoProfile -NonInteractive -EncodedCommand ( [Convert] :: ToBase64String ( [System.Text.Encoding] :: Unicode . GetBytes ( $command ) ) ) 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
throw " ' $ path' could not be removed: $ output "
}
}
}
if ( Get-LocalGroup -Name $LocalGroupName -ErrorAction SilentlyContinue ) {
Remove-LocalGroup -Name $LocalGroupName
}
foreach ( $accountSid in $Sid ) {
Remove-LocalGroupMember -SID 'S-1-5-32-544' -Member $accountSid -ErrorAction SilentlyContinue
}
}
$removeClientScript = {
param ( $PayloadPath , $Sid )
$ErrorActionPreference = 'Stop'
foreach ( $group in 'S-1-5-32-544' , 'S-1-5-32-580' ) {
foreach ( $accountSid in $Sid ) {
Remove-LocalGroupMember -SID $group -Member $accountSid -ErrorAction SilentlyContinue
}
}
Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_ . SID -in $Sid } | Remove-CimInstance
if ( Test-Path -LiteralPath $PayloadPath ) {
Remove-Item -LiteralPath $PayloadPath -Recurse -Force
}
}
$removeAccountScript = {
param ( $OrganizationalUnitName )
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$unit = Get-ADOrganizationalUnit -LDAPFilter " (ou= $ OrganizationalUnitName) " -SearchBase $domain . DistinguishedName -SearchScope OneLevel -Server $domain . PDCEmulator
if ( $unit ) {
Remove-ADOrganizationalUnit -Identity $unit -Recursive -Server $domain . PDCEmulator -Confirm : $false
}
}
#endregion Remote script blocks
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
$machines = foreach ( $name in $DomainController , $FileServer , $Client ) {
$machine = Get-LabVM -ComputerName $name
if ( -not $machine ) {
throw " The lab ' $ LabName' has no machine ' $ name'. "
}
$machine
}
if ( @ ( $machines | ForEach-Object -Process { $_ . DomainName } | Select-Object -Unique ) . Count -ne 1 ) {
throw 'The domain controller, the file server, and the client must belong to one domain.'
}
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
foreach ( $name in $ForeignDomainController ) {
$machine = Get-LabVM -ComputerName $name
if ( -not $machine ) {
throw " The lab ' $ LabName' has no machine ' $ name'. "
}
if ( $machine . DomainName -eq $machines [ 0 ] . DomainName ) {
throw " The foreign domain controller ' $ name' must belong to another domain than the file server. "
}
}
if ( @ ( $ForeignDomainController | ForEach-Object -Process { ( Get-LabVM -ComputerName $_ ) . DomainName } | Select-Object -Unique ) . Count -ne @ ( $ForeignDomainController ) . Count ) {
throw 'Each foreign domain controller must belong to a domain of its own.'
}
$helperScript = Get-Content -LiteralPath ( Join-Path -Path $PSScriptRoot -ChildPath 'NTFSSecurity.LabHelpers.ps1' ) -Raw
$labCommand = @ {
NoDisplay = $true
PassThru = $true
ErrorAction = 'Stop'
}
if ( $RemoveFixture ) {
if ( -not $PSCmdlet . ShouldProcess ( " lab ' $ LabName' " , 'Remove the accounts, the share, and the folders of the live tests' ) ) {
return
}
$accountSids = @ ( Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Read the accounts' -ScriptBlock $accountSidScript -ArgumentList $organizationalUnitName @labCommand )
$null = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Remove the share and the folders' -ScriptBlock $removeFileServerScript -ArgumentList $shareName , $shareLocalPath , $payloadPath , $localGroupName , $accountSids @labCommand
$null = Invoke-LabCommand -ComputerName $Client -ActivityName 'Remove the members and the folder' -ScriptBlock $removeClientScript -ArgumentList $payloadPath , $accountSids @labCommand
$null = Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Remove the accounts' -ScriptBlock $removeAccountScript -ArgumentList $organizationalUnitName @labCommand
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
foreach ( $computer in $ForeignDomainController ) {
$null = Invoke-LabCommand -ComputerName $computer -ActivityName 'Remove the account of another domain' -ScriptBlock $removeAccountScript -ArgumentList $organizationalUnitName @labCommand
}
Write-LabProgress " Removed the live tests from the lab ' $ LabName'. "
return
}
$labels = @ ( $Version ) + @ ( if ( $ModulePath ) { 'local' } )
if ( -not $PSCmdlet . ShouldProcess ( " lab ' $ LabName' " , " Prepare the live tests and run them against $( $labels -join ', ' ) " ) ) {
return
}
$resultFolder = Join-Path -Path $OutputPath -ChildPath ( 'Results\{0:yyyyMMdd-HHmmss}' -f ( Get-Date ) )
$packageFolder = Join-Path -Path $OutputPath -ChildPath 'Packages'
$null = New-Item -ItemType Directory -Path $resultFolder , $packageFolder -Force
Write-LabProgress " START live tests in the lab ' $ LabName' against $( $labels -join ', ' ) in $( $Edition -join ', ' ) ; results in $ resultFolder "
$modules = @ (
foreach ( $item in $Version ) {
Get-NTFSSecurityPackage -Version $item -Destination $packageFolder
}
if ( $ModulePath ) {
$folder = Join-Path -Path $packageFolder -ChildPath 'local'
if ( Test-Path -LiteralPath $folder ) {
Remove-Item -LiteralPath $folder -Recurse -Force
}
$null = New-Item -ItemType Directory -Path $folder
Copy-Item -LiteralPath ( Resolve-Path -LiteralPath $ModulePath ) . ProviderPath -Destination ( Join-Path -Path $folder -ChildPath 'NTFSSecurity' ) -Recurse
[pscustomobject] @ {
Label = 'local'
Folder = $folder
ModuleVersion = Get-LabModuleVersion -Path ( Join-Path -Path $folder -ChildPath 'NTFSSecurity' )
}
}
)
Write-LabProgress 'Preparing the accounts, the file server, and the client'
$passwords = @ { }
foreach ( $name in @ ( $roleAccounts . Values ) + $subjectAccount ) {
$passwords [ $name ] = New-LabPassword
}
$directory = Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Prepare the accounts' -ScriptBlock $accountScript -ArgumentList $organizationalUnitName , $passwords , $groupMembers @labCommand
$sids = $directory . Sids
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$foreignAccounts = @ (
for ( $index = 0 ; $index -lt @ ( $ForeignDomainController ) . Count ; $index + + ) {
$account = Invoke-LabCommand -ComputerName $ForeignDomainController [ $index ] -ActivityName 'Prepare the account of another domain' -ScriptBlock $foreignAccountScript -ArgumentList $organizationalUnitName , $foreignAccount , ( New-LabPassword ) @labCommand
[pscustomobject] @ {
DomainName = $account . DomainName
Name = $account . Name
UserPrincipalName = $account . UserPrincipalName
Sid = $account . Sid
Rights = $foreignRights [ $index % $foreignRights . Count ]
}
}
)
$localGroupSid = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Prepare the file server' -ScriptBlock $fileServerSetupScript -ArgumentList $shareName , $shareLocalPath , $payloadPath , $localGroupName , $sids [ $subjectAccount ] , @ ( $sids [ 'NtfsLiveServerAdmin' ] , $sids [ 'NtfsLiveAdmin' ] ) , $sids [ 'NtfsLiveDelegates' ] @labCommand
$null = Invoke-LabCommand -ComputerName $Client -ActivityName 'Prepare the client' -ScriptBlock $clientSetupScript -ArgumentList $payloadPath , @ ( $sids [ 'NtfsLiveDelegate' ] , $sids [ 'NtfsLiveAdmin' ] ) , @ ( $sids [ 'NtfsLiveServerAdmin' ] ) @labCommand
foreach ( $computer in $Client , $FileServer ) {
Copy-LabFileItem -Path $testFiles -ComputerName $computer -DestinationFolderPath ( Join-Path -Path $payloadPath -ChildPath 'Tests' )
}
foreach ( $module in $modules ) {
Copy-LabFileItem -Path $module . Folder -ComputerName $Client -DestinationFolderPath ( Join-Path -Path $payloadPath -ChildPath 'Modules' ) -Recurse
}
$expectedFiles = @ ( $modules | ForEach-Object -Process { Join-Path -Path $payloadPath -ChildPath ( 'Modules\{0}\NTFSSecurity\NTFSSecurity.psd1' -f $_ . Label ) } ) +
@ ( $testFiles | ForEach-Object -Process { Join-Path -Path $payloadPath -ChildPath ( 'Tests\{0}' -f ( Split-Path -Path $_ -Leaf ) ) } )
$missingFiles = @ ( Invoke-LabCommand -ComputerName $Client -ActivityName 'Check the payload' -ScriptBlock {
param ( $Path )
$Path | Where-Object -FilterScript { -not ( Test-Path -LiteralPath $_ -PathType Leaf ) }
} -ArgumentList ( , $expectedFiles ) @labCommand )
if ( $missingFiles . Count -gt 0 ) {
throw " The client lacks these files: $( $missingFiles -join ', ' ) "
}
$credentials = @ { }
foreach ( $role in $roleAccounts . Keys ) {
$userName = '{0}\{1}' -f $directory . NetBiosName , $roleAccounts [ $role ]
$credentials [ $role ] = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList $userName , $passwords [ $roleAccounts [ $role ] ]
}
$clientAddress = ( Get-LabVM -ComputerName $Client ) . IpV4Address
$fileServerFqdn = '{0}.{1}' -f $FileServer , $directory . DomainName
$subjectPrincipalName = '{0}@{1}' -f $subjectAccount , $directory . DomainName
$longPathSegments = 1 . . 6 | ForEach-Object -Process { 'Segment{0:D2}-{1}' -f $_ , ( 'x' * 40 ) }
$summary = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ( $module in $modules ) {
foreach ( $runEdition in $Edition ) {
$runId = '{0}-{1}-{2:yyyyMMddHHmmss}' -f $module . Label , $runEdition , ( Get-Date )
Write-LabProgress " Run $ {runId}: preparing the folders "
$orphan = Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Create the orphan account' -ScriptBlock $newOrphanScript -ArgumentList $organizationalUnitName , $orphanAccount , ( New-LabPassword ) @labCommand
$fixtureSids = @ {
NtfsLiveDelegates = $sids [ 'NtfsLiveDelegates' ]
NtfsLiveOuter = $sids [ 'NtfsLiveOuter' ]
LocalGroup = [string] $localGroupSid
Orphan = $orphan . Sid
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Subject = $sids [ $subjectAccount ]
}
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$fixture = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Create the folders of the run' -ScriptBlock $fixtureScript -ArgumentList $helperScript , $shareLocalPath , $runId , $fixtureSids , $subjectPrincipalName , $longPathSegments , $foreignAccounts @labCommand
$null = Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Delete the orphan account' -ScriptBlock $removeOrphanScript -ArgumentList $orphan . Guid @labCommand
$oracle = Invoke-LabCommand -ComputerName $Client -ActivityName 'Calculate the rights on the client' -ScriptBlock $clientOracleScript -ArgumentList $helperScript , $fixture . EffectiveAccessSddl , $subjectPrincipalName , $orphan . Sid @labCommand
if ( $oracle . OrphanResolved ) {
throw " The client still resolves the SID $( $orphan . Sid ) of the deleted account. "
}
if ( $fixture . FileServerRights -ne $expectedFileServerRights -or $oracle . ClientRights -ne $expectedClientRights ) {
throw ( 'The tokens of {0} grant 0x{1:X} on the file server and 0x{2:X} on the client instead of 0x{3:X} and 0x{4:X}.' -f
$subjectAccount , [long] $fixture . FileServerRights , [long] $oracle . ClientRights , $expectedFileServerRights , $expectedClientRights )
}
$configuration = [ordered] @ {
RunId = $runId
ModuleVersion = $module . ModuleVersion
Edition = $runEdition
DomainName = $directory . DomainName
Client = $Client
FileServer = $FileServer
FileServerFqdn = $fileServerFqdn
ShareName = $shareName
ShareLocalPath = $shareLocalPath
SharePath = '\\{0}\{1}\{2}' -f $fileServerFqdn , $shareName , $runId
ServerPath = $fixture . ServerPath
UnreachableServerName = $unreachableServerName
LongPath = $fixture . LongPath
EffectiveAccess = [ordered] @ {
FileServerRights = [long] $fixture . FileServerRights
ClientRights = [long] $oracle . ClientRights
}
test: extend the live tests in the lab to all cmdlet groups
Add the cases 4b (orphaned audit entry), 5 (owner), 6 (audit inheritance,
Clear-NTFSAudit, Get-NTFSInheritance), 7 (item cmdlets on the share),
8 (link cmdlets on the share), and 9 (Get-NTFSSimpleAccess), and the
accounts of three other domains and forests (-ForeignDomainController).
The fixture writes the folders with SetAccessControl instead of Set-Acl,
which also wrote an empty SACL and dropped the inherited audit entries.
The delegated account expects the denial of Get-NTFSEffectiveAccess
-ServerName, which the file server answers only for its administrators
and the members of Access Control Assistance Operators.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
ForeignAccounts = @ (
foreach ( $account in $foreignAccounts ) {
[ordered] @ {
Name = $account . Name
DomainName = $account . DomainName
Sid = $account . Sid
Rights = $account . Rights
EffectiveRights = [long] $fixture . ForeignEffectiveRights [ $account . Sid ]
}
}
)
Accounts = [ordered] @ {
Delegate = [ordered] @ { Name = $credentials [ 'Delegate' ] . UserName ; Sid = $sids [ 'NtfsLiveDelegate' ] ; ClientAdministrator = $true ; FileServerAdministrator = $false }
ServerAdmin = [ordered] @ { Name = $credentials [ 'ServerAdmin' ] . UserName ; Sid = $sids [ 'NtfsLiveServerAdmin' ] ; ClientAdministrator = $false ; FileServerAdministrator = $true }
Admin = [ordered] @ { Name = $credentials [ 'Admin' ] . UserName ; Sid = $sids [ 'NtfsLiveAdmin' ] ; ClientAdministrator = $true ; FileServerAdministrator = $true }
Server = [ordered] @ { Name = $directory . InstallName ; Sid = $directory . InstallSid ; ClientAdministrator = $true ; FileServerAdministrator = $true }
Subject = [ordered] @ { Name = '{0}\{1}' -f $directory . NetBiosName , $subjectAccount ; Sid = $sids [ $subjectAccount ] }
Orphan = [ordered] @ { Name = '{0}\{1}' -f $directory . NetBiosName , $orphanAccount ; Sid = $orphan . Sid }
}
}
$json = $configuration | ConvertTo-Json -Depth 5
Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath " $ runId.json " ) -Value $json
$configurationPath = Join-Path -Path $payloadPath -ChildPath " Configuration\ $ runId.json "
foreach ( $computer in $Client , $FileServer ) {
$null = Invoke-LabCommand -ComputerName $computer -ActivityName 'Write the configuration' -ScriptBlock $writeConfigurationScript -ArgumentList $configurationPath , $json @labCommand
}
$runs = @ (
foreach ( $role in $roleAccounts . Keys ) {
@ { Role = $role ; Edition = $runEdition ; ModulePath = Join-Path -Path $payloadPath -ChildPath ( 'Modules\{0}\NTFSSecurity' -f $module . Label ) }
}
@ { Role = 'Server' ; Edition = 'Desktop' ; ModulePath = '' }
)
foreach ( $run in $runs ) {
Write-LabProgress " Run $ {runId}: role $( $run . Role ) "
$arguments = $run . Edition , ( Join-Path -Path $payloadPath -ChildPath 'Tests' ) , $run . ModulePath , $configurationPath , $run . Role
if ( $run . Role -eq 'Server' ) {
$outcome = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Run the tests of the role Server' -ScriptBlock $runScript -ArgumentList $arguments @labCommand
}
else {
$session = New-PSSession -ComputerName $clientAddress -Credential $credentials [ $run . Role ] -Authentication Credssp
try {
$outcome = Invoke-Command -Session $session -ScriptBlock $runScript -ArgumentList $arguments
}
finally {
Remove-PSSession -Session $session
}
}
$baseName = '{0}-{1}' -f $runId , $run . Role
Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath " $ baseName.log " ) -Value $outcome . Output
$cases = @ ( )
if ( $outcome . Result ) {
Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath " $ baseName.result.json " ) -Value $outcome . Result
$cases = @ ( ConvertFrom-LabTestResult -Json $outcome . Result )
}
$failures = @ ( $cases | Where-Object -FilterScript { $_ . Result -eq 'Failed' } )
$entry = [pscustomobject] @ {
Version = $module . Label
Edition = $runEdition
Role = $run . Role
Account = $outcome . Account
ExitCode = $outcome . ExitCode
Passed = @ ( $cases | Where-Object -FilterScript { $_ . Result -eq 'Passed' } ) . Count
Failed = $failures . Count
Skipped = @ ( $cases | Where-Object -FilterScript { $_ . Result -notin 'Passed' , 'Failed' } ) . Count
Failures = @ ( $failures | ForEach-Object -Process { [pscustomobject] @ { Name = $_ . Name ; Message = $_ . Message } } )
}
$summary . Add ( $entry )
Write-LabProgress ( 'Run {0}: role {1} as {2}: {3} passed, {4} failed, {5} skipped, exit code {6}' -f
$runId , $run . Role , $entry . Account , $entry . Passed , $entry . Failed , $entry . Skipped , $entry . ExitCode )
}
$state = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Read the security descriptors of the run' -ScriptBlock $stateScript -ArgumentList $helperScript , $fixture . ServerPath @labCommand
$state | Select-Object -Property Path , Stored , Sacl | ConvertTo-Json -Depth 3 |
Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath " $ runId-State.json " )
}
}
$summary | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath 'Summary.json' )
$report = New-Object -TypeName 'System.Collections.Generic.List[string]'
$report . Add ( '# NTFSSecurity live tests' )
$report . Add ( '' )
$report . Add ( ( 'Lab `{0}`: client `{1}`, file server `{2}`, domain controller `{3}`.' -f $LabName , $Client , $FileServer , $DomainController ) )
$report . Add ( '' )
$report . Add ( '| Version | Edition | Role | Passed | Failed | Skipped |' )
$report . Add ( '| --- | --- | --- | ---: | ---: | ---: |' )
foreach ( $entry in $summary ) {
$report . Add ( ( '| {0} | {1} | {2} | {3} | {4} | {5} |' -f $entry . Version , $entry . Edition , $entry . Role , $entry . Passed , $entry . Failed , $entry . Skipped ) )
}
foreach ( $entry in $summary | Where-Object -FilterScript { $_ . Failed -gt 0 -or -not $_ . Passed } ) {
$report . Add ( '' )
$report . Add ( ( '## {0}, {1}, {2}' -f $entry . Version , $entry . Edition , $entry . Role ) )
$report . Add ( '' )
foreach ( $failure in $entry . Failures ) {
$report . Add ( ( '- {0}: {1}' -f $failure . Name , ( ( $failure . Message -split '\r?\n' ) [ 0 ] ) ) )
}
if ( -not $entry . Passed -and -not $entry . Failures ) {
$report . Add ( '- No test ran; see the log of the role.' )
}
}
Set-Content -LiteralPath ( Join-Path -Path $resultFolder -ChildPath 'Summary.md' ) -Value $report
Write-LabProgress " DONE live tests; results in $ resultFolder "
$summary